diff --git a/tests/e2e/fixtures/resume.ts b/tests/e2e/fixtures/resume.ts index 24e352621..026f3971d 100644 --- a/tests/e2e/fixtures/resume.ts +++ b/tests/e2e/fixtures/resume.ts @@ -21,12 +21,31 @@ export async function createSampleResumeFromDashboard(page: Page, testInfo: Test return resumeName; } +// Sections the Design mode hosts; every other section title lives in Write. +const designSections = new Set(["Template", "Layout", "Typography", "Design", "Page"]); +// Sections the Share & export sheet hosts, without headings of their own. +const shareSections = new Set(["Sharing", "Statistics"]); + export async function openSidebarSection(page: Page, title: string) { - // Rail nav buttons are labelled with the section title (aria-label); clicking scrolls to the section. - await page.getByRole("button", { name: title, exact: true }).first().click(); + if (shareSections.has(title)) { + // "Share (link is live)" once the resume is public. + await page.getByRole("button", { name: /^Share\b/ }).click(); + await expect(page.getByRole("dialog", { name: "Share & export" })).toBeVisible(); + return; + } + + await page.getByRole("tab", { name: designSections.has(title) ? "Design" : "Write", exact: true }).click(); // The visible section heading is exactly the title. Filter to visible because the screen-reader-only // resume mirror in the preview also renders

section headings with the same name. - await expect(page.getByRole("heading", { name: title, exact: true }).filter({ visible: true }).first()).toBeVisible(); + const heading = page.getByRole("heading", { name: title, exact: true }).filter({ visible: true }).first(); + await heading.scrollIntoViewIfNeeded(); + await expect(heading).toBeVisible(); +} + +/** Opens the Download dialog with every format from the ▾ next to Download PDF. */ +export async function openDownloadDialog(page: Page) { + await page.getByRole("button", { name: "More download formats", exact: true }).click(); + await expect(page.getByRole("dialog", { name: "Download" })).toBeVisible(); } export async function openResumeCardMenu(page: Page, resumeName: string, { reload = true } = {}) { diff --git a/tests/e2e/specs/builder-save-navigation.spec.ts b/tests/e2e/specs/builder-save-navigation.spec.ts index 8fb4830fb..37255a0d9 100644 --- a/tests/e2e/specs/builder-save-navigation.spec.ts +++ b/tests/e2e/specs/builder-save-navigation.spec.ts @@ -16,8 +16,8 @@ function waitSave(page: Page) { ); } async function clickDashboardWithoutNavigationWait(page: Page) { - const dashboardButton = page.getByRole("button", { name: "Go to resumes dashboard", exact: true }); - const box = await dashboardButton.boundingBox(); + const dashboardLink = page.getByRole("link", { name: "Back to documents", exact: true }); + const box = await dashboardLink.boundingBox(); if (!box) throw new Error("Dashboard navigation button is not visible."); await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2); } @@ -38,7 +38,7 @@ test("retries a failed autosave before leaving the builder", async ({ authPage: await route.abort("failed"); }); await page.getByLabel("Name", { exact: true }).fill("Draft recovered before leaving"); - await expect(page.getByText("Your latest changes could not be saved.", { exact: true })).toBeVisible(); + await expect(page.getByRole("status").filter({ hasText: "Not saved" })).toBeVisible(); await page.unroute(updateUrl); const arrived = barrier(); const release = barrier(); @@ -65,10 +65,10 @@ test("retains the current draft when saving during navigation fails", async ({ a await route.abort("failed"); }); await page.getByLabel("Name", { exact: true }).fill("Keep unsaved draft"); - await expect(page.getByText("Your latest changes could not be saved.", { exact: true })).toBeVisible(); + await expect(page.getByRole("status").filter({ hasText: "Not saved" })).toBeVisible(); await clickDashboardWithoutNavigationWait(page); await expect.poll(() => attempts).toBe(2); - await expect(page.getByRole("status").filter({ hasText: "Couldn't save" })).toBeVisible(); + await expect(page.getByRole("status").filter({ hasText: "Not saved" })).toBeVisible(); expect(page.url()).toBe(url); await expect(page.getByLabel("Name", { exact: true })).toHaveValue("Keep unsaved draft"); await page.unroute(updateUrl); diff --git a/tests/e2e/specs/hyphenation.spec.ts b/tests/e2e/specs/hyphenation.spec.ts index 9945587c9..cf85f45ed 100644 --- a/tests/e2e/specs/hyphenation.spec.ts +++ b/tests/e2e/specs/hyphenation.spec.ts @@ -2,7 +2,7 @@ import type { Page, TestInfo } from "@playwright/test"; import { readFile, writeFile } from "node:fs/promises"; import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs"; import { Pool } from "pg"; -import { createSampleResumeFromDashboard, openSidebarSection } from "../fixtures/resume"; +import { createSampleResumeFromDashboard, openDownloadDialog, openSidebarSection } from "../fixtures/resume"; import { expect, test } from "../fixtures/test"; const longWord = "Gewerbesteuerdurchführungsverordnung"; @@ -61,10 +61,12 @@ async function seedHyphenationResume(page: Page) { } async function downloadPdfText(page: Page, testInfo: TestInfo, name: string) { - await openSidebarSection(page, "Export"); - await page.getByRole("button", { name: /Choose PDF, DOCX, Markdown, or JSON/ }).click(); + await openDownloadDialog(page); const pending = page.waitForEvent("download"); - await page.getByRole("button", { name: "Download PDF", exact: true }).click(); + await page + .getByRole("dialog", { name: "Download" }) + .getByRole("button", { name: "Download PDF", exact: true }) + .click(); const download = await pending; const path = testInfo.outputPath(`${name}.pdf`); await download.saveAs(path); diff --git a/tests/e2e/specs/json-export-import.spec.ts b/tests/e2e/specs/json-export-import.spec.ts index fc6c07f63..7e47b8f06 100644 --- a/tests/e2e/specs/json-export-import.spec.ts +++ b/tests/e2e/specs/json-export-import.spec.ts @@ -1,14 +1,11 @@ import { readFile } from "node:fs/promises"; -import { createSampleResumeFromDashboard, openSidebarSection } from "../fixtures/resume"; +import { createSampleResumeFromDashboard, openDownloadDialog, openSidebarSection } from "../fixtures/resume"; import { expect, test } from "../fixtures/test"; test("exports and imports a resume JSON backup", async ({ authPage: page }, testInfo) => { await createSampleResumeFromDashboard(page, testInfo); - await openSidebarSection(page, "Export"); - - // Downloads now live in a dialog: open it, then trigger the JSON export. - await page.getByRole("button", { name: /Choose PDF, DOCX, Markdown, or JSON/ }).click(); + await openDownloadDialog(page); const downloadPromise = page.waitForEvent("download"); await page.getByRole("button", { name: "Download JSON" }).click(); const download = await downloadPromise; diff --git a/tests/e2e/specs/offline-fonts.spec.ts b/tests/e2e/specs/offline-fonts.spec.ts index b92661207..18525f08a 100644 --- a/tests/e2e/specs/offline-fonts.spec.ts +++ b/tests/e2e/specs/offline-fonts.spec.ts @@ -9,7 +9,7 @@ import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs"; import { assertPdfDownloadReceived } from "../fixtures/offline-font-diagnostic"; import { classifyRasterMeasurements, locatePdfMarkerBoxes } from "../fixtures/offline-font-markers"; import { offlineFontScriptSamples, seedOfflineFontResume } from "../fixtures/offline-fonts"; -import { createSampleResumeFromDashboard, openSidebarSection } from "../fixtures/resume"; +import { createSampleResumeFromDashboard, openDownloadDialog, openSidebarSection } from "../fixtures/resume"; import { expect, test } from "../fixtures/test"; const diagnosticEnabled = process.env.OFFLINE_FONT_DIAGNOSTIC === "1"; @@ -360,10 +360,12 @@ test.describe("offline font diagnostic", () => { let rasterEvidenceStatus = "not-attempted"; try { await page.goto(fixture.builderURL); - await openSidebarSection(page, "Export"); - await page.getByRole("button", { name: /Choose PDF, DOCX, Markdown, or JSON/ }).click(); + await openDownloadDialog(page); const downloadPromise = page.waitForEvent("download", { timeout: 20_000 }); - await page.getByRole("button", { name: "Download PDF", exact: true }).click(); + await page + .getByRole("dialog", { name: "Download" }) + .getByRole("button", { name: "Download PDF", exact: true }) + .click(); const download = await downloadPromise; downloadStatus = "received"; const path = testInfo.outputPath("offline-font-browser-download.pdf"); diff --git a/tests/e2e/specs/preview-direction.spec.ts b/tests/e2e/specs/preview-direction.spec.ts index 3823ce0a4..7e670b35d 100644 --- a/tests/e2e/specs/preview-direction.spec.ts +++ b/tests/e2e/specs/preview-direction.spec.ts @@ -2,15 +2,22 @@ import type { Page } from "@playwright/test"; import { Pool } from "pg"; import { expect, test } from "../fixtures/test"; +// The page is centered in the canvas beside the panel, not in the viewport. The canvas's scrollbar sits on the +// left in right-to-left layouts, so the center comes from its content box. async function expectCenteredPreview(page: Page) { const canvas = page.locator('[aria-hidden="false"] canvas').first(); await expect(canvas).toBeVisible(); await expect - .poll(async () => { - const bounds = await canvas.boundingBox(); - if (!bounds) return Number.POSITIVE_INFINITY; - return Math.abs(bounds.x + bounds.width / 2 - (page.viewportSize()?.width ?? 0) / 2); - }) + .poll(() => + canvas.evaluate((element) => { + let scroller = element.parentElement; + while (scroller && getComputedStyle(scroller).overflowY !== "auto") scroller = scroller.parentElement; + if (!scroller) return Number.POSITIVE_INFINITY; + const bounds = element.getBoundingClientRect(); + const center = scroller.getBoundingClientRect().left + scroller.clientLeft + scroller.clientWidth / 2; + return Math.abs(bounds.left + bounds.width / 2 - center); + }), + ) .toBeLessThan(1); } @@ -37,34 +44,22 @@ for (const uiLanguage of ["English", "Arabic"]) { } finally { await pool.end(); } - await page.goto(builderUrl); + // The editor has no account menu; the UI language comes from the same cookie the language picker sets. if (uiLanguage === "Arabic") { - await page.getByRole("button", { name: "Account menu", exact: true }).click(); - await page.getByRole("menuitem", { name: "Language", exact: true }).click(); - await page.getByRole("menuitemradio", { name: "Arabic", exact: true }).click(); - await expect(page.locator("html")).toHaveAttribute("dir", "rtl"); - await page.reload(); + await page.context().addCookies([{ name: "locale", value: "ar-SA", url: new URL(builderUrl).origin }]); } + await page.goto(builderUrl); + await expect(page.locator("html")).toHaveAttribute("dir", uiLanguage === "Arabic" ? "rtl" : "ltr"); await expectCenteredPreview(page); - const zoom = page.getByRole("button", { - name: uiLanguage === "Arabic" ? "مستوى التكبير" : "Zoom level", - exact: true, - }); + // The zoom bar's middle button shows "Fit" or the zoom level; its name isn't translated yet. + const zoom = page.getByRole("button", { name: "Fit page to width", exact: true }); await expect(zoom).toHaveCSS("direction", uiLanguage === "Arabic" ? "rtl" : "ltr"); - await zoom.click(); - await page - .getByRole("menuitem", { - name: uiLanguage === "Arabic" ? "الحجم الفعلي (100%)" : "Actual size (100%)", - exact: true, - }) - .click(); - await expect(zoom).toHaveText("100%"); + await expect(zoom).not.toHaveText(/%/); + await page.getByRole("button", { name: uiLanguage === "Arabic" ? "تصغير" : "Zoom out", exact: true }).click(); + await expect(zoom).toHaveText(/^\d+%$/); await expectCenteredPreview(page); await zoom.click(); - await page - .getByRole("menuitem", { name: uiLanguage === "Arabic" ? "مناسب للعرض" : "Fit to view", exact: true }) - .click(); - await expect(zoom).toHaveText("75%"); + await expect(zoom).not.toHaveText(/%/); await expectCenteredPreview(page); const direction = await page .locator('[aria-hidden="false"] canvas') diff --git a/tests/e2e/specs/preview-export-geometry.spec.ts b/tests/e2e/specs/preview-export-geometry.spec.ts index 36867b80e..74e820594 100644 --- a/tests/e2e/specs/preview-export-geometry.spec.ts +++ b/tests/e2e/specs/preview-export-geometry.spec.ts @@ -5,7 +5,7 @@ import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs"; import { Pool } from "pg"; import { defaultResumeData } from "@reactive-resume/schema/resume/default"; import { generateFilename } from "@reactive-resume/utils/file"; -import { createSampleResumeFromDashboard, openSidebarSection } from "../fixtures/resume"; +import { createSampleResumeFromDashboard, openDownloadDialog } from "../fixtures/resume"; import { expect, test } from "../fixtures/test"; type CapturedPdfWindow = Window & { resumePdfBytes?: number[] }; @@ -37,7 +37,6 @@ type PreviewPageGeometry = { type PreviewGeometry = { devicePixelRatio: number; pages: PreviewPageGeometry[]; - transform: { matrix: string; scaleX: number; scaleY: number }; viewportClip: { clientHeight: number; clientWidth: number; @@ -72,7 +71,6 @@ const SCENARIOS: GeometryScenario[] = FORMATS.flatMap((format) => // Page count, ink presence, sentinels, and export target remain strict. const PDF_COORDINATE_TOLERANCE = 0.05; const DOM_LAYOUT_TOLERANCE = 1; -const TRANSFORM_SCALE_TOLERANCE = 0.005; const RASTER_CHANNEL_TOLERANCE = 32; const RASTER_MISMATCH_RATIO_LIMIT = 0.02; const RASTER_MISMATCH_PIXEL_LIMIT = 200_000; @@ -322,26 +320,19 @@ async function capturePreviewBytes(page: Page, expectedText: string) { return { bytes, geometry }; } -async function waitForStablePreview(page: Page, expectedZoom: number) { +// The page scale sizes the canvases (there's no CSS transform); the page canvas scrolls them. +async function waitForStablePreview(page: Page, expectedZoom: number, mediaBoxWidth: number) { await page.waitForFunction( - ({ expectedZoom: targetZoom }) => { - const wrapper = document.querySelector(".react-transform-wrapper"); - const content = document.querySelector(".react-transform-component"); + ({ expectedWidth }) => { const active = document.querySelector( '[aria-hidden="false"][data-resume-preview-template="rhyhorn"]', ); const canvases = [...(active?.querySelectorAll('canvas[aria-label^="Resume page"]') ?? [])]; - if (!wrapper || !content || canvases.length === 0) return false; + let scroller = active?.parentElement ?? null; + while (scroller && getComputedStyle(scroller).overflowY !== "auto") scroller = scroller.parentElement; + if (!scroller || canvases.length === 0) return false; - const transform = getComputedStyle(content).transform; - const matrix = transform === "none" ? new DOMMatrixReadOnly() : new DOMMatrixReadOnly(transform); const signature = JSON.stringify([ - matrix.a, - matrix.b, - matrix.c, - matrix.d, - matrix.e, - matrix.f, ...canvases.flatMap((canvas) => { const rect = canvas.getBoundingClientRect(); return [rect.x, rect.y, rect.width, rect.height, canvas.width, canvas.height, canvas.toDataURL("image/png")]; @@ -354,17 +345,12 @@ async function waitForStablePreview(page: Page, expectedZoom: number) { const count = previous?.signature === signature ? previous.count + 1 : 1; (window as unknown as Record)[stateKey] = { count, signature }; + const firstWidth = Number.parseFloat(canvases[0]?.style.width ?? ""); return ( - Math.abs(matrix.a - targetZoom) < 0.005 && - Math.abs(matrix.d - targetZoom) < 0.005 && - matrix.b === 0 && - matrix.c === 0 && - wrapper.clientWidth > 0 && - wrapper.clientHeight > 0 && - count >= 3 + Math.abs(firstWidth - expectedWidth) < 1 && scroller.clientWidth > 0 && scroller.clientHeight > 0 && count >= 3 ); }, - { expectedZoom }, + { expectedWidth: mediaBoxWidth * expectedZoom }, { polling: "raf", timeout: 15_000 }, ); } @@ -373,14 +359,12 @@ function capturePreviewGeometry(page: Page): Promise { return page.evaluate(() => { const active = document.querySelector('[aria-hidden="false"][data-resume-preview-template="rhyhorn"]'); if (!active) throw new Error("Missing active Rhyhorn preview layer."); - const viewportElement = document.querySelector(".react-transform-wrapper"); - const transformElement = document.querySelector(".react-transform-component"); - if (!viewportElement || !transformElement || !viewportElement.contains(active)) { - throw new Error("Missing preview transform wrapper or clipped active layer."); + let viewportElement = active.parentElement; + while (viewportElement && getComputedStyle(viewportElement).overflowY !== "auto") { + viewportElement = viewportElement.parentElement; } + if (!viewportElement) throw new Error("Missing the page canvas around the active layer."); const viewportRect = viewportElement.getBoundingClientRect(); - const transform = getComputedStyle(transformElement).transform; - const matrix = transform === "none" ? new DOMMatrixReadOnly() : new DOMMatrixReadOnly(transform); const pages = [...active.querySelectorAll('canvas[aria-label^="Resume page"]')].map((canvas) => { const wrapper = canvas.parentElement; if (!wrapper) throw new Error("Missing preview canvas wrapper."); @@ -408,7 +392,6 @@ function capturePreviewGeometry(page: Page): Promise { return { devicePixelRatio: window.devicePixelRatio, pages, - transform: { matrix: transform, scaleX: matrix.a, scaleY: matrix.d }, viewportClip: { clientHeight: viewportElement.clientHeight, clientWidth: viewportElement.clientWidth, @@ -544,21 +527,17 @@ function expectSamePdfGeometry(preview: PdfGeometry, downloaded: PdfGeometry, la } } -async function setZoom(page: Page, zoom: 75 | 100 | 115) { - const zoomLevel = page.getByRole("button", { name: "Zoom level", exact: true }); - if (zoom === 75) { - await expect(zoomLevel).toHaveText("75%"); - return; +// At 1920px wide, Fit caps at the 150% maximum; each Zoom out step is 10%. +const ZOOM_LEVELS = [150, 100, 70] as const; + +async function setZoom(page: Page, zoom: (typeof ZOOM_LEVELS)[number]) { + const zoomLevel = page.getByRole("button", { name: "Fit page to width", exact: true }); + await zoomLevel.click(); + await expect(zoomLevel).toHaveText("Fit"); + for (let level = 150; level > zoom; level -= 10) { + await page.getByRole("button", { name: "Zoom out", exact: true }).click(); } - if (zoom === 100) { - await zoomLevel.click(); - await page.getByRole("menuitem", { name: "Actual size (100%)", exact: true }).click(); - await expect(zoomLevel).toHaveText("100%"); - return; - } - await setZoom(page, 100); - await page.getByRole("button", { name: "Zoom in", exact: true }).click(); - await expect(zoomLevel).toHaveText("115%"); + if (zoom !== 150) await expect(zoomLevel).toHaveText(`${zoom}%`); } async function runGeometryMatrix(page: Page, testInfo: TestInfo, scenarios: GeometryScenario[], expectedDpr: number) { @@ -584,31 +563,32 @@ async function runGeometryMatrix(page: Page, testInfo: TestInfo, scenarios: Geom await writeFile(testInfo.outputPath(`${scenarioName(scenario)}.preview.pdf`), previewBytes); const zoomReports: Array<{ previewGeometry: PreviewGeometry; downloadedPdf: PdfGeometry; zoom: number }> = []; - for (const zoom of [75, 100, 115] as const) { + for (const zoom of ZOOM_LEVELS) { await setZoom(page, zoom); - await waitForStablePreview(page, zoom / 100); + await waitForStablePreview(page, zoom / 100, previewPdf.pages[0]?.mediaBox.width ?? 0); const previewGeometry = await capturePreviewGeometry(page); expect(previewGeometry.devicePixelRatio).toBe(expectedDpr); expect(previewGeometry.viewport).toEqual({ height: 1000, width: 1920 }); - expect(previewGeometry.viewportClip.clientWidth).toBe(1920); - expect(previewGeometry.viewportClip.clientHeight).toBe(1000); - expect(previewGeometry.viewportClip.overflowX).toBe("hidden"); - expect(previewGeometry.viewportClip.overflowY).toBe("hidden"); - expect(previewGeometry.viewportClip.scrollWidth).toBeGreaterThanOrEqual(1920); - expect(previewGeometry.viewportClip.scrollHeight).toBeGreaterThanOrEqual(1000); - expect(Math.abs(previewGeometry.transform.scaleX - zoom / 100)).toBeLessThanOrEqual(TRANSFORM_SCALE_TOLERANCE); - expect(Math.abs(previewGeometry.transform.scaleY - zoom / 100)).toBeLessThanOrEqual(TRANSFORM_SCALE_TOLERANCE); - expect(previewGeometry.transform.matrix).toMatch(/^matrix/); + expect(previewGeometry.viewportClip.clientWidth).toBeGreaterThan(0); + expect(previewGeometry.viewportClip.clientHeight).toBeGreaterThan(0); + expect(previewGeometry.viewportClip.overflowY).toBe("auto"); + expect(previewGeometry.viewportClip.scrollWidth).toBeGreaterThanOrEqual(previewGeometry.viewportClip.clientWidth); + expect(previewGeometry.viewportClip.scrollHeight).toBeGreaterThanOrEqual( + previewGeometry.viewportClip.clientHeight, + ); const currentFixture = await readPersistedFixture(resumeId); expect(currentFixture.data, `${scenarioName(scenario)} zoom ${zoom} persisted source JSON`).toEqual(data); expect(currentFixture.revision, `${scenarioName(scenario)} zoom ${zoom} persisted source revision`).toBe( persistedFixture.revision, ); const pending = page.waitForEvent("download"); - await openSidebarSection(page, "Export"); - await page.getByRole("button", { name: "Choose PDF, DOCX, Markdown, or JSON" }).click(); - await expect(page.getByRole("tab", { name: "Resume", exact: true })).toHaveAttribute("aria-selected", "true"); - await page.getByRole("button", { name: "Download PDF", exact: true }).click(); + await openDownloadDialog(page); + const downloadDialog = page.getByRole("dialog", { name: "Download" }); + await expect(downloadDialog.getByRole("tab", { name: "Resume", exact: true })).toHaveAttribute( + "aria-selected", + "true", + ); + await downloadDialog.getByRole("button", { name: "Download PDF", exact: true }).click(); const download = await pending; expect(download.suggestedFilename(), `${scenarioName(scenario)} zoom ${zoom} export target`).toBe( expectedExportFilename, @@ -630,11 +610,12 @@ async function runGeometryMatrix(page: Page, testInfo: TestInfo, scenarios: Geom expect(pdfPage).toBeDefined(); if (!pdfPage) continue; const renderScale = pageGeometry.canvas.width / pageGeometry.canvas.cssWidth; - expect(Math.abs(pageGeometry.canvas.cssWidth - pdfPage.mediaBox.width)).toBeLessThanOrEqual( - PDF_COORDINATE_TOLERANCE, + const scale = zoom / 100; + expect(Math.abs(pageGeometry.canvas.cssWidth - pdfPage.mediaBox.width * scale)).toBeLessThanOrEqual( + DOM_LAYOUT_TOLERANCE, ); - expect(Math.abs(pageGeometry.canvas.cssHeight - pdfPage.mediaBox.height)).toBeLessThanOrEqual( - PDF_COORDINATE_TOLERANCE, + expect(Math.abs(pageGeometry.canvas.cssHeight - pdfPage.mediaBox.height * scale)).toBeLessThanOrEqual( + DOM_LAYOUT_TOLERANCE, ); expect(renderScale, `${scenarioName(scenario)} measured render scale`).toBeGreaterThan(0); expect(Math.abs(pageGeometry.canvas.width - pageGeometry.canvas.cssWidth * renderScale)).toBeLessThanOrEqual( @@ -643,15 +624,12 @@ async function runGeometryMatrix(page: Page, testInfo: TestInfo, scenarios: Geom expect(Math.abs(pageGeometry.canvas.height - pageGeometry.canvas.cssHeight * renderScale)).toBeLessThanOrEqual( DOM_LAYOUT_TOLERANCE, ); - expect( - Math.abs(pageGeometry.canvasRect.width - pdfPage.mediaBox.width * previewGeometry.transform.scaleX), - ).toBeLessThanOrEqual(DOM_LAYOUT_TOLERANCE); - expect( - Math.abs(pageGeometry.canvasRect.height - pdfPage.mediaBox.height * previewGeometry.transform.scaleY), - ).toBeLessThanOrEqual(DOM_LAYOUT_TOLERANCE); - expect( - Math.abs(pageGeometry.canvasRect.width / pageGeometry.canvas.cssWidth - previewGeometry.transform.scaleX), - ).toBeLessThanOrEqual(TRANSFORM_SCALE_TOLERANCE); + expect(Math.abs(pageGeometry.canvasRect.width - pageGeometry.canvas.cssWidth)).toBeLessThanOrEqual( + DOM_LAYOUT_TOLERANCE, + ); + expect(Math.abs(pageGeometry.canvasRect.height - pageGeometry.canvas.cssHeight)).toBeLessThanOrEqual( + DOM_LAYOUT_TOLERANCE, + ); expect(pageGeometry.wrapper.width, `${scenarioName(scenario)} wrapper width`).toBeGreaterThan(0); expect(pageGeometry.wrapper.height, `${scenarioName(scenario)} wrapper height`).toBeGreaterThan(0); expect(pageGeometry.canvas.width, `${scenarioName(scenario)} canvas bitmap width`).toBeGreaterThan(0); diff --git a/tests/e2e/specs/section-date-sorting.spec.ts b/tests/e2e/specs/section-date-sorting.spec.ts index 9c2cfba53..6dcbc5728 100644 --- a/tests/e2e/specs/section-date-sorting.spec.ts +++ b/tests/e2e/specs/section-date-sorting.spec.ts @@ -119,8 +119,8 @@ test("sorts Experience once while preserving undo, persistence, later edits, and }) .toEqual({ ids: ["current-sort", "older-sort", "unknown-sort"], currentPeriod: "2010 - 2011" }); - await page.getByRole("button", { name: "Resume options" }).click(); - await page.getByRole("menuitem", { name: "Lock" }).click(); - await page.getByRole("alertdialog").getByRole("button", { name: "Confirm" }).click(); + // The document menu locks without asking; unlocking is one click away. + await page.getByRole("button", { name: /^Document menu/ }).click(); + await page.getByRole("menuitem", { name: "Lock editing" }).click(); await expect(section.getByRole("button", { name: "Section options" })).toBeDisabled(); }); diff --git a/tests/e2e/specs/section-recovery.spec.ts b/tests/e2e/specs/section-recovery.spec.ts index bbfa44daf..22e02149d 100644 --- a/tests/e2e/specs/section-recovery.spec.ts +++ b/tests/e2e/specs/section-recovery.spec.ts @@ -2,7 +2,12 @@ import type { Page, TestInfo } from "@playwright/test"; import { readFile } from "node:fs/promises"; import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs"; import { Pool } from "pg"; -import { createSampleResumeFromDashboard, openResumeCardMenu, openSidebarSection } from "../fixtures/resume"; +import { + createSampleResumeFromDashboard, + openDownloadDialog, + openResumeCardMenu, + openSidebarSection, +} from "../fixtures/resume"; import { expect, test } from "../fixtures/test"; const summaryMarker = "RECOVERY_SUMMARY_MARKER"; @@ -86,10 +91,9 @@ function waitForResumeSave(page: Page) { }); } -async function hideStandardSection(page: Page, navigationTitle: string, title: string) { - await page.getByRole("button", { name: navigationTitle, exact: true }).first().click(); +async function hideStandardSection(page: Page, title: string) { + await openSidebarSection(page, title); const heading = page.getByRole("heading", { name: title, exact: true }).filter({ visible: true }).first(); - await expect(heading).toBeVisible(); await heading.locator("xpath=../..").getByRole("button", { name: "Section options" }).click(); const saved = waitForResumeSave(page); await page.getByRole("menuitem", { name: "Hide", exact: true }).click(); @@ -113,10 +117,12 @@ async function showSection(page: Page, title: string) { } async function downloadPdfText(page: Page, testInfo: TestInfo, name: string) { - await openSidebarSection(page, "Export"); - await page.getByRole("button", { name: /Choose PDF, DOCX, Markdown, or JSON/ }).click(); + await openDownloadDialog(page); const pending = page.waitForEvent("download"); - await page.getByRole("button", { name: "Download PDF", exact: true }).click(); + await page + .getByRole("dialog", { name: "Download" }) + .getByRole("button", { name: "Download PDF", exact: true }) + .click(); const download = await pending; const path = testInfo.outputPath(`${name}.pdf`); await download.saveAs(path); @@ -144,8 +150,8 @@ test("recovers hidden printable sections without changing authored placement", a const authoredLayout = await seedRecoveryResume(resumeId); await page.reload(); - await hideStandardSection(page, "Summary", "Recovery Summary"); - await hideStandardSection(page, "Experience", "Recovery Experience"); + await hideStandardSection(page, "Recovery Summary"); + await hideStandardSection(page, "Recovery Experience"); await hideCustomSection(page, "Recovery Custom"); await page.reload(); @@ -181,7 +187,8 @@ test("recovers hidden printable sections without changing authored placement", a await page.getByRole("button", { name: "Undo", exact: true }).click(); await expect(page.getByRole("button", { name: "Show Recovery Custom section" })).toBeVisible(); - await page.getByRole("button", { name: "Redo", exact: true }).click(); + // Redo has no button in the editor bar; ⇧⌘Z works while focus is outside fields (here, on Undo). + await page.keyboard.press("ControlOrMeta+Shift+Z"); await expect(page.getByRole("button", { name: "Show Recovery Custom section" })).toHaveCount(0); await page.getByRole("button", { name: "Undo", exact: true }).click();