From fb98f823feb2f4749d4f29d15b50e732acb7bffb Mon Sep 17 00:00:00 2001 From: wilfordgrimley <2397930+WilfordGrimley@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:33:13 +0000 Subject: [PATCH] Port/drop the 6 Proposal-H-parked Playwright spec files (issue #272) Drops CommonCardback.visual/SelectedImagesRibbon/ProjectEditorMobileScroll (genuine gaps/obsolete-layout coverage, ack-tokened); ports PDFGenerator/ PagePreview/PostExportContributionPrompt onto the standalone /print route; adds a CardSlot.spec.ts test for the rail's per-slot cardback override (Cluster-8 ruling); fixes now-stale doc references to the classic Print! tab in docs/features/pdf-generator.md and print-export-page.md. Co-Authored-By: Claude Fable 5 --- .github/coverage-acks.txt | 31 ++ .github/scripts/tests/test_coverage_delta.py | 5 +- docs/features/pdf-generator.md | 25 +- docs/features/print-export-page.md | 12 +- frontend/tests/CardSlot.spec.ts | 52 +++ frontend/tests/PDFGenerator.spec.ts | 52 +-- frontend/tests/PagePreview.spec.ts | 55 +-- .../PostExportContributionPrompt.spec.ts | 39 +- .../tests/ProjectEditorMobileScroll.spec.ts | 114 ------ frontend/tests/SelectedImagesRibbon.spec.ts | 342 ------------------ frontend/tests/test-utils.ts | 31 ++ .../visual/CommonCardback.visual.spec.ts | 83 ----- 12 files changed, 199 insertions(+), 642 deletions(-) delete mode 100644 frontend/tests/ProjectEditorMobileScroll.spec.ts delete mode 100644 frontend/tests/SelectedImagesRibbon.spec.ts delete mode 100644 frontend/tests/visual/CommonCardback.visual.spec.ts diff --git a/.github/coverage-acks.txt b/.github/coverage-acks.txt index 9bbcb4c66..0a483ea23 100644 --- a/.github/coverage-acks.txt +++ b/.github/coverage-acks.txt @@ -21,3 +21,34 @@ # coverage-ack: frontend/tests/Foo.spec.ts::some describe > * — reason # # Lines starting with `#` and blank lines are ignored. + +# Proposal H parked-spec port wave (2026-07-24, issue #272) - owner-ruled drops (file deleted, +# not just re-skipped). Full parity/upstreamability tables live in this wave's own PR body, not +# duplicated here - one line per exception, per this file's own convention above. + +# Owner-ruled DROP: the aria-snapshot coverage of the classic grid's per-slot common-cardback +# swatch has no unified-page replacement to port onto (CommonCardback.tsx's own editor mount is +# classic-/editor-only, fully unrouted since the Proposal H swap) - the owner's replacement UX +# (an export-time reminder) is a separate, not-yet-built design, tracked apart from this wave. +coverage-ack: frontend/tests/visual/CommonCardback.visual.spec.ts::* — owner-ruled DROP, replacement UX is a separate not-yet-built design (see PR body) + +# Owner-ruled DROP: all 12 tests cover bulk multi-select editing (select N slots, then bulk +# change-query/change-image/delete/select-similar/select-all) - a real, tracked capability gap on +# the unified /editor page (issue #272 item 6, no bulk-select surface exists there at all today), +# not a redundant or obsolete test. Same finding CardSlot.spec.ts's own wave-3 port comment +# already documents independently. +coverage-ack: frontend/tests/SelectedImagesRibbon.spec.ts::* — owner-ruled DROP, bulk multi-select has no unified-page equivalent yet (tracked gap, issue #272 item 6 - see PR body) + +# Owner-ruled DROP: all 4 tests assert CSS/testids (left-panel/right-panel/add-cards-panel, +# OverflowCol's chainScroll overscroll-behavior-y) that only exist on the classic, fully-unrouted +# grid layout. The unified page's own mobile fix (issue #266) is a bottom-sheet drawer, not +# stacked-panel scroll chaining, and already has its own dedicated coverage in DisplayPage.spec.ts's +# "phone viewport (issue #266)" describe block. +coverage-ack: frontend/tests/ProjectEditorMobileScroll.spec.ts::* — owner-ruled DROP, subject CSS/testids only exist on the classic unrouted layout; #266's own mobile fix has separate coverage (see PR body) + +# Un-skip + re-home port of PostExportContributionPrompt.spec.ts (issue #166) onto /print's own +# PDFGenerator mount - renamed off its stale "classic Print! tab" title (the classic grid is +# fully unrouted; PDF generation lives solely on /print now) in the same change, same convention +# PR #337 established for this file's title staleness pattern. The new title carries the +# unchanged behavior forward (un-skipped, not removed) at a new, non-stale title. +coverage-ack: frontend/tests/PostExportContributionPrompt.spec.ts::Post-export contribution prompt (issue #166) - classic Print! tab > also appears after a successful export from PDFGenerator.tsx's own classic tab — renamed + un-skipped in the same change, see the file's own new title diff --git a/.github/scripts/tests/test_coverage_delta.py b/.github/scripts/tests/test_coverage_delta.py index 5a348904c..c3a7ae2b8 100644 --- a/.github/scripts/tests/test_coverage_delta.py +++ b/.github/scripts/tests/test_coverage_delta.py @@ -310,7 +310,10 @@ def test_real_manifest_is_non_empty_and_has_known_skips(self): manifest = cd.build_manifest_from_worktree(REPO_ROOT) self.assertGreater(len(manifest), 100) skipped_files = {e.file for e in manifest.values() if e.skip} - self.assertIn("frontend/tests/PDFGenerator.spec.ts", skipped_files) + # PDFGenerator.spec.ts/PagePreview.spec.ts/PostExportContributionPrompt.spec.ts were + # un-skipped by the 2026-07-24 parked-spec port wave (issue #272) - CardImageStates.spec.ts + # is still fully skipped pending its own port, so it's the fixture here now. + self.assertIn("frontend/tests/CardImageStates.spec.ts", skipped_files) def test_perf_dir_is_excluded(self): manifest = cd.build_manifest_from_worktree(REPO_ROOT) diff --git a/docs/features/pdf-generator.md b/docs/features/pdf-generator.md index a60b18aa4..26732a18e 100644 --- a/docs/features/pdf-generator.md +++ b/docs/features/pdf-generator.md @@ -172,13 +172,17 @@ reached from `/display`'s Finish footer via a pre-print save gate; see Footer](../proposals/proposal-h-display-layout-spec.md#finish-footer-save-before-print) and [Print-Page Funnel](../proposals/proposal-h-display-layout-spec.md#print-page-funnel-destination) -decisions), so -this component's mounts are now: this tab, `PDFGeneratorModal.tsx`, and -`ProjectEditor.tsx` — one implementation, not a forked second copy. See -`docs/features/printing-tags.md`'s own entry for the full detail (why -`/whatsthat` and not a new route, the `sessionStorage`-backed "never -repeats within a session" rule) and `docs/features/print-export-page.md` -for the classic "Print!" tab's side of this. +decisions). The later Proposal H route swap (2026-07-23, issues #231/#272) +fully unrouted the classic grid `ProjectEditor.tsx` as well (component kept +in-tree, deletion is a separate later decision) — this component's only +LIVE mounts today are `FinishedMyProject.tsx`'s PDF tab (reached solely via +the standalone `/print` route, `pages/print.tsx`) and `PDFGeneratorModal.tsx` +(mounted globally via `Modals.tsx`, route-independent); one implementation +either way, not a forked second copy. See `docs/features/printing-tags.md`'s +own entry for the full detail (why `/whatsthat` and not a new route, the +`sessionStorage`-backed "never repeats within a session" rule) and +`docs/features/print-export-page.md` for the classic "Print!" tab's own +(now unrouted) history. ## Key files @@ -202,8 +206,11 @@ for the classic "Print!" tab's side of this. bug 4 (preview warning, confirm-gated download/cancel, and a real-image success-path regression check) - `frontend/tests/PostExportContributionPrompt.spec.ts` — issue #166 - coverage across both real export surfaces (`/display` and the classic - "Print!" tab) + coverage against this component's one remaining live mount, `/print`'s + PDF tab (re-homed there from the classic "Print!" tab in the 2026-07-24 + parked-spec port wave, issue #272 — the second, `/display`-inline-export + surface this file used to also cover was already retired by issue #275, + above) ## Status diff --git a/docs/features/print-export-page.md b/docs/features/print-export-page.md index d1c495bbd..eabfc6675 100644 --- a/docs/features/print-export-page.md +++ b/docs/features/print-export-page.md @@ -34,10 +34,14 @@ dismissible `Alert` shown once per session after a genuine "Generate PDF"/"Save PDF to Google Drive" success, linking to `/whatsthat`. One shared implementation (`frontend/src/features/export/ usePostExportContributionPrompt.ts` + `PostExportContributionPrompt.tsx`), mounted from `PDFGenerator.tsx` itself so every real caller of that -component — this tab (reachable both via the classic editor's "Print!" tab -and, since issue #275, standalone at `pages/print.tsx`), `PDFGeneratorModal.tsx`, -`ProjectEditor.tsx` — gets it for free, rather than wiring it into -`FinishedMyProject.tsx` separately. (Issue #275 also retired the unified +component gets it for free, rather than wiring it into `FinishedMyProject.tsx` +separately. This tab was reachable both via the classic editor's "Print!" +tab and, since issue #275, standalone at `pages/print.tsx` — the later +Proposal H route swap (2026-07-23, issues #231/#272) fully unrouted the +classic grid `ProjectEditor.tsx` (component kept in-tree, deletion is a +separate later decision), so `pages/print.tsx` is this tab's only live +route today; `PDFGeneratorModal.tsx` remains a second, route-independent +mount (via `Modals.tsx`). (Issue #275 also retired the unified `/display` page's OWN separate inline export pipeline and its own mount of this same prompt — PDF generation now lives solely here, reached from `/display`'s Finish footer via a pre-print save gate; see diff --git a/frontend/tests/CardSlot.spec.ts b/frontend/tests/CardSlot.spec.ts index 7bd101016..ebfd99371 100644 --- a/frontend/tests/CardSlot.spec.ts +++ b/frontend/tests/CardSlot.spec.ts @@ -20,6 +20,7 @@ import { import { test } from "../playwright.setup"; import { changeQueries, + ensureDisplayFace, expectDisplaySheetSlotState, expectDisplaySheetSlotToExist, expectDisplaySheetSlotToNotExist, @@ -215,6 +216,57 @@ test.describe("CardSlot", () => { await expectDisplaySheetSlotState(page, 1, "back", cardDocument2.name); }); + // Cluster-8 ruling (2026-07-24, issue #272 parked-spec port wave): CardbackToolbarButton's + // modal (the test directly above) is the project-wide canonical entry - picking there changes + // EVERY slot's default back. The rail's own per-slot Select Version section is the other + // supported entry, and is a genuine PER-SLOT OVERRIDE instead - both are supported, but they're + // not the same operation, and nothing before this test asserted the rail path leaves sibling + // slots (or the project-wide default itself) alone. + test("CardSlot's own rail Select Version section overrides just one slot's back, leaving the project-wide cardback (and sibling slots) untouched", async ({ + page, + network, + }) => { + network.use( + cardDocumentsThreeResults, + cardbacksTwoResults, + sourceDocumentsOneResult, + ...defaultHandlers + ); + await loadPageWithDefaultBackend(page); + + // Two members, both front-only queries - both backs default to the project cardback + // (cardDocument1, cardbacksTwoResults' first entry), same starting point as the toolbar test + // above. + await importTextOnEditorLanding(page, "2x my search query"); + await expectDisplaySheetSlotState(page, 1, "back", cardDocument1.name); + await expectDisplaySheetSlotState(page, 2, "back", cardDocument1.name); + + // Open slot 1's rail on its back face and pick the OTHER cardback candidate via Select + // Version - not the toolbar picker (openDisplayCardbackGridSelector above). + await ensureDisplayFace(page, "back"); + await page.getByTestId("page-preview-slot").first().click(); + await page + .getByTestId(`select-version-tile-${cardDocument2.identifier}`) + .locator(".mpccard") + .click(); + + // Slot 1's back changed... + await expectDisplaySheetSlotState(page, 1, "back", cardDocument2.name); + // ...but slot 2's back (still on the project-wide default) did not - a real per-slot + // override, not a second way to reach the same project-wide change. + await expectDisplaySheetSlotState(page, 2, "back", cardDocument1.name); + + // Confirming from the OTHER direction too: the toolbar's own picker still shows the + // untouched project-wide default as selected (the glowing-border highlight class), not + // slot 1's override. + const gridSelector = await openDisplayCardbackGridSelector(page); + await expect( + gridSelector.locator( + `[data-card-identifier="${cardDocument1.identifier}"]` + ) + ).toHaveClass(/mpccard-highlight/); + }); + test("changing a card slot's query", async ({ page, network }) => { network.use( cardDocumentsThreeResults, diff --git a/frontend/tests/PDFGenerator.spec.ts b/frontend/tests/PDFGenerator.spec.ts index cdb51c450..2a15caea6 100644 --- a/frontend/tests/PDFGenerator.spec.ts +++ b/frontend/tests/PDFGenerator.spec.ts @@ -15,27 +15,15 @@ import { } from "@/mocks/handlers"; import { test } from "../playwright.setup"; -import { - importTextOnEditorLanding, - loadPageWithDefaultBackend, -} from "./test-utils"; - -// Proposal H switchover (2026-07-23, issues #231/#272) - /editor now serves the unified -// sheet+rail page (`DisplayPage.tsx`); the classic grid `ProjectEditor` this file's own setup -// depends on (via testids/interaction patterns like `front-slot`/`back-slot`/`common-cardback`/ -// the "Add Cards" right-panel dropdown/the classic "Print!" tab, or a component with no rendered -// equivalent on the new page yet - see issue #272's own tracked parity gaps) is fully unrouted, -// not just delisted from the nav. Skipped here rather than deleted (component files themselves -// are untouched, per this swap's own scope) or silently left red - porting this coverage to -// DisplayPage's DOM is real, non-mechanical work tracked against #272, not done as part of the -// route swap itself (the owner's directive was to proceed with the swap regardless of the -// checklist's open items). -test.beforeEach(async ({}, testInfo) => { - testInfo.skip( - true, - "Proposal H switchover (2026-07-23): tests classic /editor-only UI, now unrouted - see issue #272" - ); -}); +import { navigateToPrintPDFTab } from "./test-utils"; + +// Parked-spec port wave (2026-07-24, issue #272). Re-homed onto the standalone /print route +// (D10, pages/print.tsx) - PDFGenerator.tsx itself is completely unchanged by the Proposal H +// route swap (its only live mount moved from the classic grid's "Print!" tab to /print's +// FinishedMyProject, both the same unforked component), so every assertion below (testids, +// modal/toast copy, localStorage key, timing) is unchanged from the pre-swap version of this +// file - only the navigation helper (navigateToPrintPDFTab, test-utils.ts) differs. +test.describe.configure({ timeout: 60_000 }); // Matches the domains configured via NEXT_PUBLIC_IMAGE_WORKER_URL / // NEXT_PUBLIC_IMAGE_BUCKET_URL in playwright.config.ts's webServer env. @@ -75,12 +63,8 @@ const imageBucketSuccess = http.get( }) ); -const addCardAndOpenPDFTab = async (page: Page) => { - await loadPageWithDefaultBackend(page); - await importTextOnEditorLanding(page, "my search query"); - await page.getByRole("tab", { name: "Print!" }).click(); - await page.getByRole("tab", { name: "PDF" }).click(); -}; +const addCardAndOpenPDFTab = async (page: Page) => + navigateToPrintPDFTab(page, "my search query"); test.describe("PDFGenerator - card image fetch failures", () => { test("warns in the live preview when a card image fails to fetch", async ({ @@ -289,16 +273,12 @@ test.describe("PDFGenerator - manual bleed override (Proposal B PR-2)", () => { .toBe(JSON.stringify({ [cardDocument1.identifier]: "force-bleed" })); // A fresh navigation rather than page.reload() - reload() alone was observed to hang past - // the test timeout in this app; waiting for "domcontentloaded" rather than the default - // "load" avoids a second hang, both unrelated to anything this PR touches (this app's + // the test timeout in this app, unrelated to anything this PR touches (this app's // webworkers appear not to settle a second "load" event cleanly within one Playwright page). - await page.goto("/editor?server=http://127.0.0.1:8000", { - waitUntil: "domcontentloaded", - }); - await page.getByText("Choose Art").click(); - await importTextOnEditorLanding(page, "my search query"); - await page.getByRole("tab", { name: "Print!" }).click(); - await page.getByRole("tab", { name: "PDF" }).click(); + // The project itself doesn't persist across this reload (only the standalone localStorage + // override does), so the whole navigateToPrintPDFTab journey - including re-importing - runs + // again from scratch, same as every other test in this file. + await navigateToPrintPDFTab(page, "my search query"); await page.getByText("Bleed Overrides").click(); await expect( diff --git a/frontend/tests/PagePreview.spec.ts b/frontend/tests/PagePreview.spec.ts index 262709693..e690bf636 100644 --- a/frontend/tests/PagePreview.spec.ts +++ b/frontend/tests/PagePreview.spec.ts @@ -7,34 +7,37 @@ import { } from "@/mocks/handlers"; import { test } from "../playwright.setup"; -import { - importTextOnEditorLanding, - loadPageWithDefaultBackend, -} from "./test-utils"; +import { navigateToPrintPDFTab } from "./test-utils"; -// Proposal H switchover (2026-07-23, issues #231/#272) - /editor now serves the unified -// sheet+rail page (`DisplayPage.tsx`); the classic grid `ProjectEditor` this file's own setup -// depends on (via testids/interaction patterns like `front-slot`/`back-slot`/`common-cardback`/ -// the "Add Cards" right-panel dropdown/the classic "Print!" tab, or a component with no rendered -// equivalent on the new page yet - see issue #272's own tracked parity gaps) is fully unrouted, -// not just delisted from the nav. Skipped here rather than deleted (component files themselves -// are untouched, per this swap's own scope) or silently left red - porting this coverage to -// DisplayPage's DOM is real, non-mechanical work tracked against #272, not done as part of the -// route swap itself (the owner's directive was to proceed with the swap regardless of the -// checklist's open items). -test.beforeEach(async ({}, testInfo) => { - testInfo.skip( - true, - "Proposal H switchover (2026-07-23): tests classic /editor-only UI, now unrouted - see issue #272" - ); -}); +// Parked-spec port wave (2026-07-24, issue #272). Re-homed onto the standalone /print route +// (D10, pages/print.tsx) - see PDFGenerator.spec.ts's own module comment for the full rationale +// (this file exercises the identical PDFGenerator.tsx mount, unchanged by the route swap). +// +// Ported selectively, not verbatim - deduped against the unified page's own sheet coverage +// (DisplayPage.spec.ts/OrphanRendering.spec.ts, both dozens of `page-preview-slot` assertions +// against the SAME PagePreview.tsx component, just mounted directly on /editor's sheet region +// rather than behind the Print page's PDF tab). None of this file's 3 tests turned out to be a +// true duplicate once checked against that existing coverage - all 3 ported as-is (see this PR's +// own body for the full dedup table): +// - "shows the fast DOM preview by default..." asserts the `page-preview` CONTAINER testid, +// which neither DisplayPage.spec.ts nor OrphanRendering.spec.ts ever reference (both only +// ever assert against individual `page-preview-slot` children) - not a duplicate. +// - "toggling to exact preview switches to the pdf.js canvas render, and back" exercises +// `preview-mode-toggle`, which only exists on PDFGenerator.tsx's own mount - DisplayPage's +// sheet has no toggle at all (it's permanently in fast-preview mode). Print-page-only +// behavior, not portable elsewhere, not a duplicate. +// - "the fast preview reflows live when page margins change..." exercises the "Spacing & +// Margins" NumericField section, which is also PDFGenerator.tsx-only - DisplayPage has no +// margin controls of its own (grep-confirmed: "Spacing & Margins"/"page margin" only appear +// in PDFGenerator.tsx). Not a duplicate. +// +// Generous file-level timeout (not the 30s default) - navigateToPrintPDFTab's own retry against +// /print's cold-compile race (test-utils.ts's own comment) needs headroom beyond a single 30s +// test timeout to actually get a second attempt in, same precedent PDFGenerator.spec.ts uses. +test.describe.configure({ timeout: 60_000 }); -const addCardAndOpenPDFTab = async (page: Page) => { - await loadPageWithDefaultBackend(page); - await importTextOnEditorLanding(page, "my search query"); - await page.getByRole("tab", { name: "Print!" }).click(); - await page.getByRole("tab", { name: "PDF" }).click(); -}; +const addCardAndOpenPDFTab = async (page: Page) => + navigateToPrintPDFTab(page, "my search query"); test.describe("PDFGenerator - fast page preview (Proposal A)", () => { test("shows the fast DOM preview by default, with a slot rendered and no spinner", async ({ diff --git a/frontend/tests/PostExportContributionPrompt.spec.ts b/frontend/tests/PostExportContributionPrompt.spec.ts index 27bbf1652..c63a6e6ac 100644 --- a/frontend/tests/PostExportContributionPrompt.spec.ts +++ b/frontend/tests/PostExportContributionPrompt.spec.ts @@ -12,28 +12,11 @@ import { } from "@/mocks/handlers"; import { test } from "../playwright.setup"; -import { - importTextOnEditorLanding, - loadPageWithDefaultBackend, -} from "./test-utils"; - -// Proposal H switchover (2026-07-23, issues #231/#272) - /editor now serves the unified -// sheet+rail page (`DisplayPage.tsx`); the classic grid `ProjectEditor` this file's own setup -// depends on (via testids/interaction patterns like `front-slot`/`back-slot`/`common-cardback`/ -// the "Add Cards" right-panel dropdown/the classic "Print!" tab, or a component with no rendered -// equivalent on the new page yet - see issue #272's own tracked parity gaps) is fully unrouted, -// not just delisted from the nav. Skipped here rather than deleted (component files themselves -// are untouched, per this swap's own scope) or silently left red - porting this coverage to -// DisplayPage's DOM is real, non-mechanical work tracked against #272, not done as part of the -// route swap itself (the owner's directive was to proceed with the swap regardless of the -// checklist's open items). -test.beforeEach(async ({}, testInfo) => { - testInfo.skip( - true, - "Proposal H switchover (2026-07-23): tests classic /editor-only UI, now unrouted - see issue #272" - ); -}); +import { navigateToPrintPDFTab } from "./test-utils"; +// Parked-spec port wave (2026-07-24, issue #272). Re-homed onto the standalone /print route +// (D10, pages/print.tsx) - see PDFGenerator.spec.ts's own module comment for the full rationale. +// // Issue #166 - the post-export contribution prompt. Mounted from PDFGenerator.tsx (so the // classic "Print!" tab / Print page gets it). It used to ALSO be mounted from DisplayPage.tsx's // own inline export (Proposal H, item 2) - issue #275 removed that inline pipeline entirely (PDF @@ -74,16 +57,18 @@ const oneCardHandlers = [ ...defaultHandlers, ]; -test.describe("Post-export contribution prompt (issue #166) - classic Print! tab", () => { - test("also appears after a successful export from PDFGenerator.tsx's own classic tab", async ({ +// Generous file-level timeout (not the 30s default) - navigateToPrintPDFTab's own retry against +// /print's cold-compile race (test-utils.ts's own comment) needs headroom beyond a single 30s +// test timeout to actually get a second attempt in, same precedent PDFGenerator.spec.ts uses. +test.describe.configure({ timeout: 60_000 }); + +test.describe("Post-export contribution prompt (issue #166) - /print PDF tab", () => { + test("appears after a successful export from PDFGenerator.tsx's own /print mount", async ({ page, network, }) => { network.use(imageBucketSuccess, imageWorkerSuccess, ...oneCardHandlers); - await loadPageWithDefaultBackend(page); - await importTextOnEditorLanding(page, "my search query"); - await page.getByRole("tab", { name: "Print!" }).click(); - await page.getByRole("tab", { name: "PDF" }).click(); + await navigateToPrintPDFTab(page, "my search query"); const prompt = page.getByTestId("post-export-contribution-prompt"); await expect(prompt).not.toBeVisible(); diff --git a/frontend/tests/ProjectEditorMobileScroll.spec.ts b/frontend/tests/ProjectEditorMobileScroll.spec.ts deleted file mode 100644 index 1751e7bf6..000000000 --- a/frontend/tests/ProjectEditorMobileScroll.spec.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { expect } from "@playwright/test"; - -import { - cardDocumentsThreeResults, - defaultHandlers, - searchResultsThreeResults, - sourceDocumentsOneResult, -} from "@/mocks/handlers"; - -import { test } from "../playwright.setup"; -import { importText, loadPageWithDefaultBackend } from "./test-utils"; - -// Proposal H switchover (2026-07-23, issues #231/#272) - /editor now serves the unified -// sheet+rail page (`DisplayPage.tsx`); the classic grid `ProjectEditor` this file's own setup -// depends on (via testids/interaction patterns like `front-slot`/`back-slot`/`common-cardback`/ -// the "Add Cards" right-panel dropdown/the classic "Print!" tab, or a component with no rendered -// equivalent on the new page yet - see issue #272's own tracked parity gaps) is fully unrouted, -// not just delisted from the nav. Skipped here rather than deleted (component files themselves -// are untouched, per this swap's own scope) or silently left red - porting this coverage to -// DisplayPage's DOM is real, non-mechanical work tracked against #272, not done as part of the -// route swap itself (the owner's directive was to proceed with the swap regardless of the -// checklist's open items). -test.beforeEach(async ({}, testInfo) => { - testInfo.skip( - true, - "Proposal H switchover (2026-07-23): tests classic /editor-only UI, now unrouted - see issue #272" - ); -}); - -const threeCardHandlers = [ - cardDocumentsThreeResults, - sourceDocumentsOneResult, - searchResultsThreeResults, - ...defaultHandlers, -]; - -// Real touch-driven scroll chaining (the actual mechanism this fix addresses) can't be -// exercised by a synthetic browser-automation event - Chromium doesn't drive compositor-level -// touch scrolling from JS-dispatched events the way a genuine OS touch gesture does. These -// tests verify the CSS/layout precondition for chaining to work (overscroll-behavior-y: auto, -// not none) and that the panel is structurally reachable - see this PR's merge-time checklist -// for the real-device confirmation this can't replace. -test.describe("Editor - mobile scroll chaining (item 5)", () => { - test("at a mobile viewport, the stacked panels allow scroll to chain instead of trapping it", async ({ - page, - network, - }) => { - network.use(...threeCardHandlers); - await page.setViewportSize({ width: 390, height: 844 }); - await loadPageWithDefaultBackend(page); - await importText(page, "my search query"); - - const leftPanel = page.getByTestId("left-panel"); - const rightPanel = page.getByTestId("right-panel"); - - const leftOverscroll = await leftPanel.evaluate( - (el) => getComputedStyle(el).overscrollBehaviorY - ); - const rightOverscroll = await rightPanel.evaluate( - (el) => getComputedStyle(el).overscrollBehaviorY - ); - expect(leftOverscroll).toBe("auto"); - expect(rightOverscroll).toBe("auto"); - }); - - test("at a mobile viewport, 'I've Finished My Project' is reachable once the card grid is scrolled past", async ({ - page, - network, - }) => { - network.use(...threeCardHandlers); - await page.setViewportSize({ width: 390, height: 844 }); - await loadPageWithDefaultBackend(page); - await importText(page, "my search query"); - - const finishedButton = page.getByText("I've Finished My Project"); - await finishedButton.scrollIntoViewIfNeeded(); - await expect(finishedButton).toBeVisible(); - await expect(finishedButton).toBeInViewport(); - }); - - test("at desktop width, overscroll-behavior-y stays auto on the side-by-side panels too (unaffected by the mobile fix)", async ({ - page, - network, - }) => { - network.use(...threeCardHandlers); - await loadPageWithDefaultBackend(page); - await importText(page, "my search query"); - - const leftPanel = page.getByTestId("left-panel"); - const rightPanel = page.getByTestId("right-panel"); - const leftOverscroll = await leftPanel.evaluate( - (el) => getComputedStyle(el).overscrollBehaviorY - ); - const rightOverscroll = await rightPanel.evaluate( - (el) => getComputedStyle(el).overscrollBehaviorY - ); - expect(leftOverscroll).toBe("auto"); - expect(rightOverscroll).toBe("auto"); - }); - - test("other OverflowCol usages (e.g. the Add Cards panel) keep overscroll-behavior-y: none, unaffected by this fix", async ({ - page, - network, - }) => { - network.use(...defaultHandlers); - await loadPageWithDefaultBackend(page); - - const addCardsPanel = page.getByTestId("add-cards-panel"); - const overscroll = await addCardsPanel.evaluate( - (el) => getComputedStyle(el).overscrollBehaviorY - ); - expect(overscroll).toBe("none"); - }); -}); diff --git a/frontend/tests/SelectedImagesRibbon.spec.ts b/frontend/tests/SelectedImagesRibbon.spec.ts deleted file mode 100644 index 95d77241a..000000000 --- a/frontend/tests/SelectedImagesRibbon.spec.ts +++ /dev/null @@ -1,342 +0,0 @@ -import { expect } from "@playwright/test"; - -import { - cardDocument1, - cardDocument2, - cardDocument3, - cardDocument4, - cardDocument5, -} from "@/common/test-constants"; -import { - cardbacksOneOtherResult, - cardbacksOneResult, - cardbacksTwoResults, - cardDocumentsOneResult, - cardDocumentsSixResults, - cardDocumentsThreeResults, - defaultHandlers, - dfcPairsMatchingCards1And4, - searchResultsForDFCMatchedCards1And4, - searchResultsOneResult, - searchResultsSixResults, - searchResultsThreeResults, - sourceDocumentsOneResult, -} from "@/mocks/handlers"; - -import { test } from "../playwright.setup"; -import { - changeImageForSelectedImages, - changeQueryForSelectedImages, - deleteSelectedImages, - deselectSlot, - expectCardGridSlotState, - expectCardSlotToNotExist, - importText, - loadPageWithDefaultBackend, - selectAll, - selectSimilar, - selectSlot, -} from "./test-utils"; - -// Proposal H switchover (2026-07-23, issues #231/#272) - /editor now serves the unified -// sheet+rail page (`DisplayPage.tsx`); the classic grid `ProjectEditor` this file's own setup -// depends on (via testids/interaction patterns like `front-slot`/`back-slot`/`common-cardback`/ -// the "Add Cards" right-panel dropdown/the classic "Print!" tab, or a component with no rendered -// equivalent on the new page yet - see issue #272's own tracked parity gaps) is fully unrouted, -// not just delisted from the nav. Skipped here rather than deleted (component files themselves -// are untouched, per this swap's own scope) or silently left red - porting this coverage to -// DisplayPage's DOM is real, non-mechanical work tracked against #272, not done as part of the -// route swap itself (the owner's directive was to proceed with the swap regardless of the -// checklist's open items). -test.beforeEach(async ({}, testInfo) => { - testInfo.skip( - true, - "Proposal H switchover (2026-07-23): tests classic /editor-only UI, now unrouted - see issue #272" - ); -}); - -test.describe("SelectedImagesRibbon", () => { - test("selecting a single card and changing its query", async ({ - page, - network, - }) => { - network.use( - cardDocumentsSixResults, - cardbacksOneOtherResult, - sourceDocumentsOneResult, - searchResultsSixResults, - ...defaultHandlers - ); - await loadPageWithDefaultBackend(page); - - await importText(page, "query 1"); - await expectCardGridSlotState(page, 1, "front", cardDocument1.name, 1, 1); - - await selectSlot(page, 1, "front"); - await changeQueryForSelectedImages(page, "query 2"); - await expectCardGridSlotState(page, 1, "front", cardDocument2.name, 1, 1); - }); - - test("selecting multiple cards and changing both of their queries", async ({ - page, - network, - }) => { - network.use( - cardDocumentsSixResults, - cardbacksOneOtherResult, - sourceDocumentsOneResult, - searchResultsSixResults, - ...defaultHandlers - ); - await loadPageWithDefaultBackend(page); - - await importText(page, "2x query 1"); - await expectCardGridSlotState(page, 1, "front", cardDocument1.name, 1, 1); - await expectCardGridSlotState(page, 2, "front", cardDocument1.name, 1, 1); - - await selectSlot(page, 1, "front"); - await selectSlot(page, 2, "front"); - await changeQueryForSelectedImages(page, "query 2"); - await expectCardGridSlotState(page, 1, "front", cardDocument2.name, 1, 1); - await expectCardGridSlotState(page, 2, "front", cardDocument2.name, 1, 1); - }); - - test("selecting a single card and changing its selected image", async ({ - page, - network, - }) => { - network.use( - cardDocumentsThreeResults, - cardbacksOneOtherResult, - sourceDocumentsOneResult, - searchResultsThreeResults, - ...defaultHandlers - ); - await loadPageWithDefaultBackend(page); - - await importText(page, "my search query"); - await expectCardGridSlotState(page, 1, "front", cardDocument1.name, 1, 3); - - await selectSlot(page, 1, "front"); - await changeImageForSelectedImages(page, cardDocument2.name); - await expectCardGridSlotState(page, 1, "front", cardDocument2.name, 2, 3); - }); - - test("selecting multiple cards with the same query and changing both of their selected images", async ({ - page, - network, - }) => { - network.use( - cardDocumentsThreeResults, - cardbacksOneOtherResult, - sourceDocumentsOneResult, - searchResultsThreeResults, - ...defaultHandlers - ); - await loadPageWithDefaultBackend(page); - - await importText(page, "2x my search query"); - await expectCardGridSlotState(page, 1, "front", cardDocument1.name, 1, 3); - await expectCardGridSlotState(page, 2, "front", cardDocument1.name, 1, 3); - - await selectSlot(page, 1, "front"); - await selectSlot(page, 2, "front"); - await changeImageForSelectedImages(page, cardDocument2.name); - await expectCardGridSlotState(page, 1, "front", cardDocument2.name, 2, 3); - await expectCardGridSlotState(page, 2, "front", cardDocument2.name, 2, 3); - }); - - test("selecting multiple cardbacks and changing both of their selected images", async ({ - page, - network, - }) => { - network.use( - cardDocumentsThreeResults, - cardbacksTwoResults, - sourceDocumentsOneResult, - searchResultsThreeResults, - ...defaultHandlers - ); - await loadPageWithDefaultBackend(page); - - await importText(page, "2x my search query"); - await expectCardGridSlotState(page, 1, "back", cardDocument1.name, 1, 2); - await expectCardGridSlotState(page, 2, "back", cardDocument1.name, 1, 2); - - await selectSlot(page, 1, "back"); - await selectSlot(page, 2, "back"); - await changeImageForSelectedImages(page, cardDocument2.name); - await expectCardGridSlotState(page, 1, "back", cardDocument2.name, 2, 2); - await expectCardGridSlotState(page, 2, "back", cardDocument2.name, 2, 2); - }); - - test("cannot change the images of multiple selected images when they don't share the same query", async ({ - page, - network, - }) => { - network.use( - cardDocumentsSixResults, - cardbacksOneOtherResult, - sourceDocumentsOneResult, - searchResultsSixResults, - ...defaultHandlers - ); - await loadPageWithDefaultBackend(page); - - await importText(page, "query 1\nquery 2"); - await expectCardGridSlotState(page, 1, "front", cardDocument1.name, 1, 1); - await expectCardGridSlotState(page, 2, "front", cardDocument2.name, 1, 1); - - await selectSlot(page, 1, "front"); - await selectSlot(page, 2, "front"); - - await expect(page.getByText("Change Version")).not.toBeVisible(); - }); - - test("selecting a single card and deleting it", async ({ page, network }) => { - network.use( - cardDocumentsOneResult, - cardbacksOneOtherResult, - sourceDocumentsOneResult, - searchResultsOneResult, - ...defaultHandlers - ); - await loadPageWithDefaultBackend(page); - - await importText(page, "my search query"); - await expectCardGridSlotState(page, 1, "front", cardDocument1.name, 1, 1); - - await selectSlot(page, 1, "front"); - await deleteSelectedImages(page); - await expectCardSlotToNotExist(page, 1); - }); - - test("selecting multiple cards and deleting them", async ({ - page, - network, - }) => { - network.use( - cardDocumentsOneResult, - cardbacksOneOtherResult, - sourceDocumentsOneResult, - searchResultsOneResult, - ...defaultHandlers - ); - await loadPageWithDefaultBackend(page); - - await importText(page, "2x my search query"); - await expectCardGridSlotState(page, 1, "front", cardDocument1.name, 1, 1); - await expectCardGridSlotState(page, 2, "front", cardDocument1.name, 1, 1); - - await selectSlot(page, 1, "front"); - await selectSlot(page, 2, "front"); - await deleteSelectedImages(page); - await expectCardSlotToNotExist(page, 1); - await expectCardSlotToNotExist(page, 2); - }); - - test("selecting then clearing the selection", async ({ page, network }) => { - network.use( - cardDocumentsOneResult, - cardbacksOneResult, - sourceDocumentsOneResult, - searchResultsOneResult, - ...defaultHandlers - ); - await loadPageWithDefaultBackend(page); - - await importText(page, "my search query"); - await expectCardGridSlotState(page, 1, "front", cardDocument1.name, 1, 1); - - await selectSlot(page, 1, "front"); - await deselectSlot(page, 1, "front"); - }); - - test("selecting then expanding the selection to similar front images", async ({ - page, - network, - }) => { - network.use( - cardDocumentsSixResults, - cardbacksOneOtherResult, - sourceDocumentsOneResult, - searchResultsSixResults, - ...defaultHandlers - ); - await loadPageWithDefaultBackend(page); - - await importText(page, "2x query 1\n1x query 2"); - - await selectSlot(page, 1, "front"); - await selectSimilar(page); - - const element1 = page.getByLabel("select-front0").locator("*").first(); - const element2 = page.getByLabel("select-front1").locator("*").first(); - const element3 = page.getByLabel("select-front2").locator("*").first(); - await expect(element1).toHaveClass(/bi-check-square/); - await expect(element2).toHaveClass(/bi-check-square/); - // slot 3 should not have been selected - await expect(element3).not.toHaveClass(/bi-check-square/); - }); - - test("selecting then expanding the selection to similar back images", async ({ - page, - network, - }) => { - network.use( - cardDocumentsSixResults, - cardbacksOneOtherResult, - sourceDocumentsOneResult, - searchResultsForDFCMatchedCards1And4, - dfcPairsMatchingCards1And4, - ...defaultHandlers - ); - await loadPageWithDefaultBackend(page); - - await importText(page, "1x my search query\n2x card 3"); - // slot 1 uses dfc-pair matching to pair cards 1 and 4, while slots 2 and 3 display card 3 and use the project back - await expectCardGridSlotState(page, 1, "front", cardDocument1.name, 1, 1); - await expectCardGridSlotState(page, 1, "back", cardDocument4.name, 1, 1); - await expectCardGridSlotState(page, 2, "front", cardDocument3.name, 1, 1); - await expectCardGridSlotState(page, 2, "back", cardDocument5.name, 1, 1); - await expectCardGridSlotState(page, 3, "front", cardDocument3.name, 1, 1); - await expectCardGridSlotState(page, 3, "back", cardDocument5.name, 1, 1); - - await selectSlot(page, 2, "back"); - await selectSimilar(page); - - const element1 = page.getByLabel("select-back0").locator("*").first(); - const element2 = page.getByLabel("select-back1").locator("*").first(); - const element3 = page.getByLabel("select-back2").locator("*").first(); - await expect(element2).toHaveClass(/bi-check-square/); - await expect(element3).toHaveClass(/bi-check-square/); - // slot 1's back should remain unselected - await expect(element1).not.toHaveClass(/bi-check-square/); - }); - - test("selecting then expanding the selection to all front images", async ({ - page, - network, - }) => { - network.use( - cardDocumentsSixResults, - cardbacksOneOtherResult, - sourceDocumentsOneResult, - searchResultsSixResults, - ...defaultHandlers - ); - await loadPageWithDefaultBackend(page); - - await importText(page, "2x query 1\n1x query 2"); - - await selectSlot(page, 1, "front"); - await selectAll(page); - - const element1 = page.getByLabel("select-front0").locator("*").first(); - const element2 = page.getByLabel("select-front1").locator("*").first(); - const element3 = page.getByLabel("select-front2").locator("*").first(); - await expect(element1).toHaveClass(/bi-check-square/); - await expect(element2).toHaveClass(/bi-check-square/); - await expect(element3).toHaveClass(/bi-check-square/); - }); -}); diff --git a/frontend/tests/test-utils.ts b/frontend/tests/test-utils.ts index 9ddf16e50..c99626b70 100644 --- a/frontend/tests/test-utils.ts +++ b/frontend/tests/test-utils.ts @@ -243,6 +243,37 @@ export const openSelectVersionSection = async (page: Page) => { // toggle is gone entirely, hard-pinned true) - no "Compressed" click needed any more. }; +// Parked-spec port wave (2026-07-24, issue #272; PDFGenerator/PagePreview/PostExportContribution- +// Prompt specs). PDF generation now lives solely on the standalone /print route (D10, +// pages/print.tsx) - the classic grid's own "Print!" tab this helper used to click through +// (ProjectEditor.tsx's own PrintPanel) no longer exists anywhere reachable. The unified page's +// Finish footer button is the one live entry point (mirrors UnsavedWorkGuard.spec.ts's and +// DisplayFinishFooter.spec.ts's own precedent for this exact transition) - `whoamiAnonymous` is +// part of `defaultHandlers`, so this always lands straight on /print with no pre-print save gate +// to dismiss first. FinishedMyProject's default tab is "pringleprints", not "pdf" +// (FinishedMyProject.tsx) - the PDF tab always needs an explicit click even though its own nav +// item is already visible immediately. +// +// The click+waitForURL step is wrapped in a `toPass` retry (same resilience pattern +// `openAddCardsDropdown` above already established for this suite) rather than a single +// generous-timeout attempt - observed directly (2026-07-24, verifying this port at 4 parallel +// workers) failing with a bare `net::ERR_ABORTED` on the FIRST attempt under worker contention +// (three separate spec files - this one, PDFGenerator.spec.ts, PostExportContributionPrompt.spec.ts +// - all racing to first-hit /print's cold on-demand dev-mode compile simultaneously, the same +// characteristic DisplayFinishFooter.spec.ts's own `mode: "serial"` comment documents, just not +// fully solved by that file-local fix once MULTIPLE files contend for the same route). A retried +// click safely re-fires `router.push("/print")` if the first attempt's navigation never actually +// landed - idempotent either way. +export const navigateToPrintPDFTab = async (page: Page, query: string) => { + await loadPageWithDefaultBackend(page); + await importTextOnEditorLanding(page, query); + await expect(async () => { + await page.getByTestId("finish-footer-print-export").click(); + await page.waitForURL(/\/print/, { timeout: 15_000 }); + }).toPass({ timeout: 45_000 }); + await page.getByRole("tab", { name: "PDF" }).click(); +}; + export async function expectCardSlotToExist(page: Page, slot: number) { await expect(page.getByTestId(`front-slot${slot - 1}`)).toContainText( `Slot ${slot}` diff --git a/frontend/tests/visual/CommonCardback.visual.spec.ts b/frontend/tests/visual/CommonCardback.visual.spec.ts deleted file mode 100644 index 6b18eead6..000000000 --- a/frontend/tests/visual/CommonCardback.visual.spec.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { expect } from "@playwright/test"; - -import { cardDocument1 } from "@/common/test-constants"; -import { - cardbacksOneResult, - cardbacksTwoResults, - cardDocumentsOneResult, - defaultHandlers, - sourceDocumentsOneResult, -} from "@/mocks/handlers"; - -import { test } from "../../playwright.setup"; -import { - expectCardbackSlotState, - loadPageWithDefaultBackend, -} from "../test-utils"; - -// Proposal H switchover (2026-07-23, issues #231/#272) - /editor now serves the unified -// sheet+rail page (`DisplayPage.tsx`); the classic grid `ProjectEditor` this file's own setup -// depends on (via testids/interaction patterns like `front-slot`/`back-slot`/`common-cardback`/ -// the "Add Cards" right-panel dropdown/the classic "Print!" tab, or a component with no rendered -// equivalent on the new page yet - see issue #272's own tracked parity gaps) is fully unrouted, -// not just delisted from the nav. Skipped here rather than deleted (component files themselves -// are untouched, per this swap's own scope) or silently left red - porting this coverage to -// DisplayPage's DOM is real, non-mechanical work tracked against #272, not done as part of the -// route swap itself (the owner's directive was to proceed with the swap regardless of the -// checklist's open items). -test.beforeEach(async ({}, testInfo) => { - testInfo.skip( - true, - "Proposal H switchover (2026-07-23): tests classic /editor-only UI, now unrouted - see issue #272" - ); -}); - -test.describe("CommonCardback visual tests", () => { - test("common cardback with single search result", async ({ - page, - network, - }) => { - network.use( - cardDocumentsOneResult, - sourceDocumentsOneResult, - cardbacksOneResult, - ...defaultHandlers - ); - await loadPageWithDefaultBackend(page); - - await expectCardbackSlotState(page, cardDocument1.name, 1, 1); - - await expect(page.getByTestId("common-cardback")).toMatchAriaSnapshot(` - - paragraph: Cardback - - img "Card 1" - - text: Card 1 - - paragraph: /Source 1 \\[\\d+ DPI\\]/ - - paragraph: 1 / 1 - `); - }); - - test("common cardback with multiple search results", async ({ - page, - network, - }) => { - network.use( - cardDocumentsOneResult, - sourceDocumentsOneResult, - cardbacksTwoResults, - ...defaultHandlers - ); - await loadPageWithDefaultBackend(page); - - await expectCardbackSlotState(page, cardDocument1.name, 1, 2); - - await expect(page.getByTestId("common-cardback")).toMatchAriaSnapshot(` - - paragraph: Cardback - - img "Card 1" - - text: Card 1 - - paragraph: /Source 1 \\[\\d+ DPI\\]/ - - button "1 / 2" - - button "❮" - - button "❯" - `); - }); -});