mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-10-02 17:54:22 +10:00
test(e2e): drive the new editor bar, modes and share sheet
openSidebarSection now switches to the mode that hosts a section or opens the Share & export sheet, and openDownloadDialog opens every format from the Download PDF split button. Specs follow the back link, the save status line, the document menu, the zoom bar and page-scale zoom.
This commit is contained in:
@@ -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 <h2> 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 } = {}) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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<HTMLElement>(".react-transform-wrapper");
|
||||
const content = document.querySelector<HTMLElement>(".react-transform-component");
|
||||
({ expectedWidth }) => {
|
||||
const active = document.querySelector<HTMLElement>(
|
||||
'[aria-hidden="false"][data-resume-preview-template="rhyhorn"]',
|
||||
);
|
||||
const canvases = [...(active?.querySelectorAll<HTMLCanvasElement>('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<string, unknown>)[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<PreviewGeometry> {
|
||||
return page.evaluate(() => {
|
||||
const active = document.querySelector<HTMLElement>('[aria-hidden="false"][data-resume-preview-template="rhyhorn"]');
|
||||
if (!active) throw new Error("Missing active Rhyhorn preview layer.");
|
||||
const viewportElement = document.querySelector<HTMLElement>(".react-transform-wrapper");
|
||||
const transformElement = document.querySelector<HTMLElement>(".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<HTMLCanvasElement>('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<PreviewGeometry> {
|
||||
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);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user