diff --git a/frontend/tests/CardImageStates.spec.ts b/frontend/tests/CardImageStates.spec.ts
index 11f445086..1e2037b6e 100644
--- a/frontend/tests/CardImageStates.spec.ts
+++ b/frontend/tests/CardImageStates.spec.ts
@@ -11,20 +11,24 @@ import {
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).
+// Parity wave 2 investigation (2026-07-23, issue #272) - confirmed still a genuine gap, not just a
+// route-swap casualty: this file's `card-image-error-placeholder`/`card-image-slow-load-hint`
+// testids only exist in `Card.tsx` (grep confirms - src/features/card/Card.tsx). The unified
+// page's sheet slots are NOT Card.tsx mounts - they're `PagePreview.tsx`'s own
+// `page-preview-slot`, which already has its own project-level `loading`/`failed` states (no
+// candidate resolved yet, or none found at all - see DisplaySlotStates.spec.ts's own coverage of
+// those) but a plain, unwrapped `
` for the "a candidate IS
+// resolved, but its own image fetch 404s or hangs" case this file tests - no onError placeholder
+// swap, no slow-load hint timer, at all. Porting this coverage isn't a test-authoring exercise (no
+// existing DisplayPage surface to point it at) - it would mean adding real new
+// fetch-failure/slow-load handling to PagePreview.tsx first, which is out of this wave's "port
+// existing coverage" scope. Left skipped, not silently dropped - worth a real issue of its own if
+// the owner wants sheet-level image-fetch-failure UX built (distinct from #272's tracked gaps,
+// none of which mention this specifically).
test.beforeEach(async ({}, testInfo) => {
testInfo.skip(
true,
- "Proposal H switchover (2026-07-23): tests classic /editor-only UI, now unrouted - see issue #272"
+ "Issue #272 wave 2 (2026-07-23): PagePreview.tsx's sheet slots have no error-fetch/slow-load handling to port this onto - a genuine, confirmed gap, not a route-swap casualty. See this file's own header comment."
);
});
diff --git a/frontend/tests/ChangeQueryModal.spec.ts b/frontend/tests/ChangeQueryModal.spec.ts
index fe259454e..1b1eff713 100644
--- a/frontend/tests/ChangeQueryModal.spec.ts
+++ b/frontend/tests/ChangeQueryModal.spec.ts
@@ -22,33 +22,28 @@ import {
import { test } from "../playwright.setup";
import {
changeQueries,
- enableFuzzySearch,
- expectCardGridSlotState,
- expectCardSlotToExist,
- importText,
+ enableDisplayFuzzySearch,
+ ensureDisplayFace,
+ expectDisplaySheetSlotState,
+ expectDisplaySheetSlotToExist,
+ importTextOnEditorLanding,
loadPageWithDefaultBackend,
- openChangeQueryModal,
- selectSlot,
- toggleFace,
+ openDisplayChangeQueryModal,
} 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"
- );
-});
-
+// Parity wave 2 (2026-07-23, issue #272): ported onto the unified `/editor` page. The classic
+// grid opened this modal by clicking a slot's own query text; ChangeQueryModal.tsx itself is
+// unchanged (still globally mounted, Modals.tsx) - only how it's reached differs, via the shared
+// CardSlotContextMenu's "Change Query" action (openDisplayChangeQueryModal, test-utils.ts).
+//
+// Dropped, not ported: the 3 multi-slot-selection tests ("plural text when multiple front slots
+// are selected", "updates all fronts and backs for a multi-slot selection", "not shown when any
+// one of the selected slots already has the DFC back query") depended on the classic grid's
+// checkbox multi-select + SelectedImagesRibbon's own "Change Query" trigger - bulk multi-select
+// has no equivalent on the unified page (issue #272 item 6, still not built; SelectedImagesRibbon
+// itself is parked, not ported, this same wave - see this PR's own description). The single-slot
+// DFC prompt/submission/condition/fuzzy-search/checkbox-reset coverage below is unaffected and
+// fully proves the same underlying logic.
test.describe("ChangeQueryModal tests", () => {
test("change one card's query", async ({ page, network }) => {
network.use(
@@ -59,26 +54,22 @@ test.describe("ChangeQueryModal tests", () => {
);
await loadPageWithDefaultBackend(page);
- await importText(
+ await importTextOnEditorLanding(
page,
`query 1${SelectedImageSeparator}${cardDocument1.identifier}`
);
- await expectCardSlotToExist(page, 1);
- await expectCardGridSlotState(page, 1, "front", cardDocument1.name, 1, 1);
+ await expectDisplaySheetSlotToExist(page, 1);
+ await expectDisplaySheetSlotState(page, 1, "front", cardDocument1.name);
// change query - type in "query 2"
- const modal = await openChangeQueryModal(
- page,
- "front-slot0",
- cardDocument1.name
- );
+ const modal = await openDisplayChangeQueryModal(page, 1);
await expect(
modal.getByLabel("change-selected-image-queries-text")
).toHaveValue("query 1");
await changeQueries(page, "query 2");
// expect the slot to have changed from card 1 to card 2
- await expectCardGridSlotState(page, 1, "front", cardDocument2.name, 1, 1);
+ await expectDisplaySheetSlotState(page, 1, "front", cardDocument2.name);
});
});
@@ -105,13 +96,9 @@ test.describe("ChangeQueryModal DFC pair tests", () => {
...defaultHandlers
);
await loadPageWithDefaultBackend(page);
- await importText(page, "query 1");
+ await importTextOnEditorLanding(page, "query 1");
- const modal = await openChangeQueryModal(
- page,
- "front-slot0",
- cardDocument1.name
- );
+ const modal = await openDisplayChangeQueryModal(page, 1);
await modal
.getByLabel("change-selected-image-queries-text")
.fill("query 2");
@@ -127,13 +114,9 @@ test.describe("ChangeQueryModal DFC pair tests", () => {
}) => {
network.use(...dfcHandlers);
await loadPageWithDefaultBackend(page);
- await importText(page, "card 3");
+ await importTextOnEditorLanding(page, "card 3");
- const modal = await openChangeQueryModal(
- page,
- "front-slot0",
- cardDocument3.name
- );
+ const modal = await openDisplayChangeQueryModal(page, 1);
await modal
.getByLabel("change-selected-image-queries-text")
.fill("my search query");
@@ -154,34 +137,6 @@ test.describe("ChangeQueryModal DFC pair tests", () => {
// Checkbox is unchecked by default
await expect(modal.getByLabel("Update back")).not.toBeChecked();
});
-
- test("DFC prompt shown with correct plural text when multiple front slots are selected", async ({
- page,
- network,
- }) => {
- network.use(...dfcHandlers);
- await loadPageWithDefaultBackend(page);
- await importText(page, "2x card 3");
-
- await selectSlot(page, 1, "front");
- await selectSlot(page, 2, "front");
- await page.getByText("Change Query").click();
- const modal = page.getByTestId("change-query-modal");
-
- await modal
- .getByLabel("change-selected-image-queries-text")
- .fill("my search query");
-
- await expect(
- modal.getByText("matches a double-faced card pair")
- ).toBeVisible();
- // Plural phrasing
- await expect(
- modal.getByText(/update the backs of the selected slots to/i)
- ).toBeVisible();
- // Checkbox label is plural
- await expect(modal.getByLabel("Update backs")).toBeVisible();
- });
});
test.describe("submission behaviour", () => {
@@ -191,15 +146,15 @@ test.describe("ChangeQueryModal DFC pair tests", () => {
}) => {
network.use(...dfcHandlers);
await loadPageWithDefaultBackend(page);
- await importText(page, "card 3");
- await expectCardGridSlotState(page, 1, "front", cardDocument3.name, 1, 1);
- await expectCardGridSlotState(page, 1, "back", cardDocument5.name, 1, 1);
-
- const modal = await openChangeQueryModal(
- page,
- "front-slot0",
- cardDocument3.name
- );
+ await importTextOnEditorLanding(page, "card 3");
+ await expectDisplaySheetSlotState(page, 1, "front", cardDocument3.name);
+ await expectDisplaySheetSlotState(page, 1, "back", cardDocument5.name);
+ // expectDisplaySheetSlotState's own back-face check above leaves the sheet showing backs -
+ // reset to fronts before right-clicking, since openDisplayChangeQueryModal's context menu
+ // reads whichever face is currently displayed.
+ await ensureDisplayFace(page, "front");
+
+ const modal = await openDisplayChangeQueryModal(page, 1);
await modal
.getByLabel("change-selected-image-queries-text")
.fill("my search query");
@@ -209,9 +164,9 @@ test.describe("ChangeQueryModal DFC pair tests", () => {
// Leave checkbox unchecked and submit
await page.getByLabel("change-selected-image-queries-submit").click();
- await expectCardGridSlotState(page, 1, "front", cardDocument1.name, 1, 1);
+ await expectDisplaySheetSlotState(page, 1, "front", cardDocument1.name);
// Back must remain unchanged
- await expectCardGridSlotState(page, 1, "back", cardDocument5.name, 1, 1);
+ await expectDisplaySheetSlotState(page, 1, "back", cardDocument5.name);
});
test("submitting with the DFC checkbox updates both front and back queries", async ({
@@ -220,46 +175,17 @@ test.describe("ChangeQueryModal DFC pair tests", () => {
}) => {
network.use(...dfcHandlers);
await loadPageWithDefaultBackend(page);
- await importText(page, "card 3");
+ await importTextOnEditorLanding(page, "card 3");
- const modal = await openChangeQueryModal(
- page,
- "front-slot0",
- cardDocument3.name
- );
+ const modal = await openDisplayChangeQueryModal(page, 1);
await modal
.getByLabel("change-selected-image-queries-text")
.fill("my search query");
await modal.getByLabel("Update back").check();
await page.getByLabel("change-selected-image-queries-submit").click();
- await expectCardGridSlotState(page, 1, "front", cardDocument1.name, 1, 1);
- await expectCardGridSlotState(page, 1, "back", cardDocument4.name, 1, 1);
- });
-
- test("submitting with the DFC checkbox updates all fronts and backs for a multi-slot selection", async ({
- page,
- network,
- }) => {
- network.use(...dfcHandlers);
- await loadPageWithDefaultBackend(page);
- await importText(page, "2x card 3");
-
- await selectSlot(page, 1, "front");
- await selectSlot(page, 2, "front");
- await page.getByText("Change Query").click();
- const modal = page.getByTestId("change-query-modal");
-
- await modal
- .getByLabel("change-selected-image-queries-text")
- .fill("my search query");
- await modal.getByLabel("Update backs").check();
- await page.getByLabel("change-selected-image-queries-submit").click();
-
- await expectCardGridSlotState(page, 1, "front", cardDocument1.name, 1, 1);
- await expectCardGridSlotState(page, 2, "front", cardDocument1.name, 1, 1);
- await expectCardGridSlotState(page, 1, "back", cardDocument4.name, 1, 1);
- await expectCardGridSlotState(page, 2, "back", cardDocument4.name, 1, 1);
+ await expectDisplaySheetSlotState(page, 1, "front", cardDocument1.name);
+ await expectDisplaySheetSlotState(page, 1, "back", cardDocument4.name);
});
});
@@ -270,15 +196,12 @@ test.describe("ChangeQueryModal DFC pair tests", () => {
}) => {
network.use(...dfcHandlers);
await loadPageWithDefaultBackend(page);
- await importText(page, "card 3");
-
- // Open the Change Query modal for the back slot
- await toggleFace(page);
- const modal = await openChangeQueryModal(
- page,
- "back-slot0",
- cardDocument5.name
- );
+ await importTextOnEditorLanding(page, "card 3");
+
+ // Open the Change Query modal for the back slot - openDisplayChangeQueryModal's own
+ // right-click reads whichever face is currently displayed.
+ await ensureDisplayFace(page, "back");
+ const modal = await openDisplayChangeQueryModal(page, 1);
await modal
.getByLabel("change-selected-image-queries-text")
.fill("my search query");
@@ -295,16 +218,12 @@ test.describe("ChangeQueryModal DFC pair tests", () => {
network.use(...dfcHandlers);
await loadPageWithDefaultBackend(page);
// Importing via text with DFC pairs active auto-sets the back to "Card 4"
- await importText(page, "my search query");
- await expectCardGridSlotState(page, 1, "front", cardDocument1.name, 1, 1);
- await expectCardGridSlotState(page, 1, "back", cardDocument4.name, 1, 1);
+ await importTextOnEditorLanding(page, "my search query");
+ await expectDisplaySheetSlotState(page, 1, "front", cardDocument1.name);
+ await expectDisplaySheetSlotState(page, 1, "back", cardDocument4.name);
// Now open the modal and type the same DFC front query again
- const modal = await openChangeQueryModal(
- page,
- "front-slot0",
- cardDocument1.name
- );
+ const modal = await openDisplayChangeQueryModal(page, 1);
await modal
.getByLabel("change-selected-image-queries-text")
.fill("my search query");
@@ -314,32 +233,6 @@ test.describe("ChangeQueryModal DFC pair tests", () => {
modal.getByText("matches a double-faced card pair")
).not.toBeVisible();
});
-
- test("DFC prompt not shown when any one of the selected slots already has the DFC back query", async ({
- page,
- network,
- }) => {
- network.use(...dfcHandlers);
- await loadPageWithDefaultBackend(page);
- // Slot 1: DFC import → back is already "Card 4"
- // Slot 2: plain import → back is project cardback (Card 5)
- await importText(page, "1x my search query\n1x card 3");
- await expectCardGridSlotState(page, 1, "back", cardDocument4.name, 1, 1);
-
- await selectSlot(page, 1, "front");
- await selectSlot(page, 2, "front");
- await page.getByText("Change Query").click();
- const modal = page.getByTestId("change-query-modal");
-
- await modal
- .getByLabel("change-selected-image-queries-text")
- .fill("my search query");
-
- // Slot 1's back already equals the DFC back — prompt must not appear for either slot
- await expect(
- modal.getByText("matches a double-faced card pair")
- ).not.toBeVisible();
- });
});
test.describe("fuzzy search", () => {
@@ -349,15 +242,11 @@ test.describe("ChangeQueryModal DFC pair tests", () => {
}) => {
network.use(...dfcHandlers);
await loadPageWithDefaultBackend(page);
- await importText(page, "card 3");
+ await importTextOnEditorLanding(page, "card 3");
- await enableFuzzySearch(page);
+ await enableDisplayFuzzySearch(page);
- const modal = await openChangeQueryModal(
- page,
- "front-slot0",
- cardDocument3.name
- );
+ const modal = await openDisplayChangeQueryModal(page, 1);
// "my search" is a prefix of the DFC front key "my search query"
await modal
.getByLabel("change-selected-image-queries-text")
@@ -374,14 +263,10 @@ test.describe("ChangeQueryModal DFC pair tests", () => {
}) => {
network.use(...dfcHandlers);
await loadPageWithDefaultBackend(page);
- await importText(page, "card 3");
+ await importTextOnEditorLanding(page, "card 3");
// Precise (non-fuzzy) search is the default — no need to configure it
- const modal = await openChangeQueryModal(
- page,
- "front-slot0",
- cardDocument3.name
- );
+ const modal = await openDisplayChangeQueryModal(page, 1);
await modal
.getByLabel("change-selected-image-queries-text")
.fill("my search");
@@ -399,14 +284,10 @@ test.describe("ChangeQueryModal DFC pair tests", () => {
}) => {
network.use(...dfcHandlers);
await loadPageWithDefaultBackend(page);
- await importText(page, "card 3");
+ await importTextOnEditorLanding(page, "card 3");
// Open the modal, trigger the DFC prompt, check the box, then close without submitting
- let modal = await openChangeQueryModal(
- page,
- "front-slot0",
- cardDocument3.name
- );
+ let modal = await openDisplayChangeQueryModal(page, 1);
await modal
.getByLabel("change-selected-image-queries-text")
.fill("my search query");
@@ -415,11 +296,7 @@ test.describe("ChangeQueryModal DFC pair tests", () => {
await modal.getByLabel("Close").click();
// Reopen the modal and trigger the DFC prompt again
- modal = await openChangeQueryModal(
- page,
- "front-slot0",
- cardDocument3.name
- );
+ modal = await openDisplayChangeQueryModal(page, 1);
await modal
.getByLabel("change-selected-image-queries-text")
.fill("my search query");
diff --git a/frontend/tests/GeneralUIAccessibility.spec.ts b/frontend/tests/GeneralUIAccessibility.spec.ts
index 47dd09c31..7705f50b7 100644
--- a/frontend/tests/GeneralUIAccessibility.spec.ts
+++ b/frontend/tests/GeneralUIAccessibility.spec.ts
@@ -8,7 +8,10 @@ import {
} from "@/mocks/handlers";
import { test } from "../playwright.setup";
-import { importText, loadPageWithDefaultBackend } from "./test-utils";
+import {
+ importTextOnEditorLanding,
+ loadPageWithDefaultBackend,
+} from "./test-utils";
const threeCardHandlers = [
cardDocumentsThreeResults,
@@ -17,114 +20,79 @@ const threeCardHandlers = [
...defaultHandlers,
];
-// Proposal H switchover (2026-07-23, issues #231/#272) - /editor now serves the unified page;
-// the classic grid `front-slot`/`left-panel`/`right-panel` testids these tests target have no
-// equivalent there, so they're individually skipped below rather than the whole file - the last
-// three describe blocks (About page, Home page logo, and the home-page half of Console warning
-// regressions) don't touch /editor at all and are unaffected.
+// Parity wave 2 (2026-07-23, issue #272): the classic grid's `front-slot`/`left-panel`/
+// `right-panel` testids have no equivalent on the unified page - the last three describe blocks
+// (About page, Home page logo, and the home-page half of Console warning regressions) don't touch
+// /editor at all and were always unaffected.
test.describe("Card slot controls - accessibility", () => {
- test("the more-options button has an accessible name", async ({
+ // Ported onto PagePreview.tsx's own per-slot menu cue (`page-preview-slot-menu-cue`, "Open card
+ // menu") - the sheet's closest equivalent to the classic grid's per-slot "more options" button
+ // (same corner-affordance role: F6/D22's own module comment calls it the "touch-discoverable
+ // menu cue", reserving the same physical corner classic CardSlot's 3-dot button occupied).
+ test("the slot menu cue has an accessible name", async ({
page,
network,
- }, testInfo) => {
- testInfo.skip(
- true,
- "Proposal H switchover (2026-07-23): targets classic /editor-only front-slot testid, now unrouted - see issue #272"
- );
+ }) => {
network.use(...threeCardHandlers);
await loadPageWithDefaultBackend(page);
- await importText(page, "my search query");
-
- const moreOptionsButton = page
- .getByTestId("front-slot0")
- .getByTestId("more-select-options");
- await expect(moreOptionsButton).toBeVisible();
- await expect(moreOptionsButton).toHaveAccessibleName("More options");
+ await importTextOnEditorLanding(page, "my search query");
+
+ const menuCue = page
+ .getByTestId("page-preview-slot")
+ .first()
+ .getByTestId("page-preview-slot-menu-cue");
+ await expect(menuCue).toBeVisible();
+ await expect(menuCue).toHaveAccessibleName("Open card menu");
});
- test("the select/remove buttons meet a comfortable touch-target size and have a visible focus style", async ({
+ // The classic grid's per-slot `.card-select` checkbox (bulk multi-select) has no equivalent on
+ // the unified page (SelectedImagesRibbon/bulk multi-select - issue #272 item 6, still not
+ // built, parked rather than ported this same wave - see this PR's own description) - the same
+ // menu cue button above is the nearest actionable per-slot control left to hold this touch-
+ // target/focus-style invariant to.
+ test("the slot menu cue has a visible focus style", async ({
page,
network,
- }, testInfo) => {
- testInfo.skip(
- true,
- "Proposal H switchover (2026-07-23): targets classic /editor-only front-slot testid, now unrouted - see issue #272"
- );
+ }) => {
network.use(...threeCardHandlers);
await loadPageWithDefaultBackend(page);
- await importText(page, "my search query");
-
- const selectButton = page
- .getByTestId("front-slot0")
- .locator(".card-select");
- const box = await selectButton.boundingBox();
- expect(box).not.toBeNull();
- expect(box!.width).toBeGreaterThanOrEqual(40);
- expect(box!.height).toBeGreaterThanOrEqual(40);
-
- // A prior mouse click (e.g. inside importText's flow) sets Chromium's input modality to
- // "mouse", under which a plain .focus() call does not match :focus-visible. Pressing Tab
- // first re-establishes keyboard modality, matching how a real keyboard user would land here.
+ await importTextOnEditorLanding(page, "my search query");
+
+ const menuCue = page
+ .getByTestId("page-preview-slot")
+ .first()
+ .getByTestId("page-preview-slot-menu-cue");
+
+ // A prior mouse click (e.g. inside importTextOnEditorLanding's flow) sets Chromium's input
+ // modality to "mouse", under which a plain .focus() call does not match :focus-visible.
+ // Pressing Tab first re-establishes keyboard modality, matching how a real keyboard user
+ // would land here.
await page.keyboard.press("Tab");
- await selectButton.focus();
- const outlineStyle = await selectButton.evaluate(
+ await menuCue.focus();
+ const outlineStyle = await menuCue.evaluate(
(el) => getComputedStyle(el).outlineStyle
);
expect(outlineStyle).not.toBe("none");
});
});
-test.describe("Editor - mobile layout", () => {
- test("at a mobile viewport, the settings panel stacks below the card grid instead of splitting the screen 50/50", async ({
- page,
- network,
- }, testInfo) => {
- testInfo.skip(
- true,
- "Proposal H switchover (2026-07-23): targets classic /editor-only left-panel/right-panel testids, now unrouted - see issue #272"
- );
- 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 leftBox = await leftPanel.boundingBox();
- const rightBox = await rightPanel.boundingBox();
- expect(leftBox).not.toBeNull();
- expect(rightBox).not.toBeNull();
-
- // Stacked (not side-by-side) means both panels span (close to) the full viewport width,
- // rather than splitting it in half.
- expect(leftBox!.width).toBeGreaterThan(350);
- expect(rightBox!.width).toBeGreaterThan(350);
- });
-
- test("at desktop width, the settings panel still sits beside the card grid (unaffected by the mobile change)", async ({
- page,
- network,
- }, testInfo) => {
- testInfo.skip(
- true,
- "Proposal H switchover (2026-07-23): targets classic /editor-only left-panel/right-panel testids, now unrouted - see issue #272"
- );
- network.use(...threeCardHandlers);
- // default chromium project viewport (800x600) is above the md breakpoint
- await loadPageWithDefaultBackend(page);
- await importText(page, "my search query");
-
- const leftPanel = page.getByTestId("left-panel");
- const rightPanel = page.getByTestId("right-panel");
- const leftBox = await leftPanel.boundingBox();
- const rightBox = await rightPanel.boundingBox();
- expect(leftBox).not.toBeNull();
- expect(rightBox).not.toBeNull();
- // Side-by-side means roughly the same vertical position and non-overlapping x-ranges.
- expect(Math.abs(leftBox!.y - rightBox!.y)).toBeLessThan(5);
- expect(leftBox!.x + leftBox!.width).toBeLessThanOrEqual(rightBox!.x + 1);
- });
-});
+// Dropped, not ported: the classic test's "comfortable touch-target size" (>=40x40px) half of
+// this coverage. PagePreview.tsx renders the sheet near print-scale (millimetre-driven sizing,
+// not a UI-scale button grid) - the menu cue measured ~11x11px here, genuinely smaller than the
+// WCAG-informed 40px target the classic grid's own full-UI-scale button met. That's a real,
+// structural difference in what this element IS (a small on-page-preview affordance vs. a
+// full-size toolbar button), not an accessibility regression this port should paper over by
+// weakening the assertion's threshold - the focus-visible-style half above is unaffected and
+// still fully verified.
+
+// "Editor - mobile layout"'s two classic-grid tests (left-panel/right-panel 50/50-split-vs-
+// stacked at mobile/desktop widths) are dropped, not ported: they verified the SAME "mobile
+// scroll affordances -> #266's responsive layer" replacement issue #272's own body already lists
+// as an intentional replacement, not a gap (ProjectEditorMobileScroll.spec.ts, this same wave's
+// PR description). The unified page's rails are off-canvas drawers (LeftRailOffcanvas/
+// RightRailOffcanvas) below their own breakpoints regardless of viewport - there's no persistent
+// 50/50 split to ever "stack" in the first place, so this specific invariant has no equivalent
+// question to ask on /editor.
test.describe("Console warning regressions", () => {
test("the home page renders with none of the previously-observed console warnings", async ({
@@ -162,11 +130,7 @@ test.describe("Console warning regressions", () => {
test("the editor page renders with none of the previously-observed console warnings", async ({
page,
network,
- }, testInfo) => {
- testInfo.skip(
- true,
- "Proposal H switchover (2026-07-23): importText is classic /editor-only UI, now unrouted - see issue #272"
- );
+ }) => {
network.use(...threeCardHandlers);
const warnings: string[] = [];
page.on("console", (msg) => {
@@ -176,7 +140,7 @@ test.describe("Console warning regressions", () => {
});
await loadPageWithDefaultBackend(page);
- await importText(page, "my search query");
+ await importTextOnEditorLanding(page, "my search query");
await page.waitForTimeout(1500);
const flagged = [
diff --git a/frontend/tests/InvalidIdentifiersStatus.spec.ts b/frontend/tests/InvalidIdentifiersStatus.spec.ts
index 27bbce4df..2e8659fba 100644
--- a/frontend/tests/InvalidIdentifiersStatus.spec.ts
+++ b/frontend/tests/InvalidIdentifiersStatus.spec.ts
@@ -17,30 +17,20 @@ import {
import { test } from "../playwright.setup";
import {
- changeQuery,
- expectCardGridSlotState,
- expectCardSlotToExist,
- importText,
+ changeQueries,
+ expectDisplaySheetSlotState,
+ expectDisplaySheetSlotToExist,
+ importTextOnEditorLanding,
loadPageWithDefaultBackend,
+ openDisplayChangeQueryModal,
} 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"
- );
-});
-
+// Parity wave 2 (2026-07-23, issue #272): ported onto the unified `/editor` page.
+// InvalidIdentifiersStatus.tsx itself is unchanged and unforked - DisplayPage.tsx's own comment
+// (issue #267 D13): "mounted unmodified in both the populated-state action bar and the
+// empty-project DeckInputLanding" - only the RIGHT-RAIL Status row (a separate, still-unbuilt
+// placement - issue #272 item 2's own remaining scope) is missing; the landing/search-bar half
+// this file exercises already works today.
test.describe("InvalidIdentifiersStatus tests", () => {
const testCases = [
{
@@ -74,9 +64,9 @@ test.describe("InvalidIdentifiersStatus tests", () => {
);
await loadPageWithDefaultBackend(page);
- await importText(page, query);
- await expectCardSlotToExist(page, 1);
- await expectCardGridSlotState(page, 1, "front", cardDocument1.name, 1, 1);
+ await importTextOnEditorLanding(page, query);
+ await expectDisplaySheetSlotToExist(page, 1);
+ await expectDisplaySheetSlotState(page, 1, "front", cardDocument1.name);
if (problematicImageCount > 0) {
const warningText = await page
@@ -107,17 +97,18 @@ test.describe("InvalidIdentifiersStatus tests", () => {
);
await loadPageWithDefaultBackend(page);
- await importText(
+ await importTextOnEditorLanding(
page,
`query 1${SelectedImageSeparator}${cardDocument1.identifier}`
);
- await expectCardSlotToExist(page, 1);
- await expectCardGridSlotState(page, 1, "front", cardDocument1.name, 1, 1);
+ await expectDisplaySheetSlotToExist(page, 1);
+ await expectDisplaySheetSlotState(page, 1, "front", cardDocument1.name);
// change query - type in "query 2"
- await changeQuery(page, "front-slot0", cardDocument1.name, "query 2");
+ await openDisplayChangeQueryModal(page, 1);
+ await changeQueries(page, "query 2");
// expect the slot to have changed from card 1 to card 2
- await expectCardGridSlotState(page, 1, "front", cardDocument2.name, 1, 1);
+ await expectDisplaySheetSlotState(page, 1, "front", cardDocument2.name);
// expect the invalid card warning to *not* have been raised
await expect(
diff --git a/frontend/tests/SelectVersionSection.spec.ts b/frontend/tests/SelectVersionSection.spec.ts
index 2489e52eb..b1e72d2c6 100644
--- a/frontend/tests/SelectVersionSection.spec.ts
+++ b/frontend/tests/SelectVersionSection.spec.ts
@@ -26,23 +26,11 @@ import {
import { test } from "../playwright.setup";
import { openSelectVersionSection } 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"
- );
-});
-
+// Parity wave 2 (2026-07-23, issue #272) - un-skipped. This file was never actually classic-
+// editor-only: it already exercised the unified page's own rail (via openSelectVersionSection,
+// test-utils.ts) from the moment it landed with issue #167 (#198), before the route swap even
+// happened. It picked up the swap's blanket per-file skip marker anyway and was deliberately left
+// for this wave rather than wave 1 - see test-utils.ts's own openSelectVersionSection comment.
function buildRoute(route: string): string {
return `${localBackendURL}/${route}`;
}
diff --git a/frontend/tests/Toasts.spec.ts b/frontend/tests/Toasts.spec.ts
index cd26d02ea..959754806 100644
--- a/frontend/tests/Toasts.spec.ts
+++ b/frontend/tests/Toasts.spec.ts
@@ -19,18 +19,21 @@ import {
import { test } from "../playwright.setup";
import {
- getAddCardsMenu,
getErrorToast,
importTextOnEditorLanding,
loadPageWithDefaultBackend,
- openImportTextModal,
} from "./test-utils";
-// Proposal H switchover (2026-07-23, issues #231/#272) - /editor now serves the unified page,
-// which has no equivalent of the classic editor's "Add Cards" dropdown (openImportTextModal/
-// getAddCardsMenu, both classic-only) - 3 tests below that trigger their fetch through that
-// dropdown are individually skipped rather than the whole file, since the other tests here
-// (triggered by plain page load or the unified page's own inline importer) are unaffected.
+// Parity wave 2 (2026-07-23, issue #272): the 3 tests below (DFCPairs/importSites/sampleCards)
+// used openImportTextModal/getAddCardsMenu to OPEN the classic grid's "Add Cards" dropdown, which
+// has no equivalent on the unified page - but that click was only ever a means to mount
+// ImportText.tsx/ImportURL.tsx, whose own `useGetDFCPairsQuery`/`useGetSampleCardsQuery`/
+// `useGetImportSitesQuery` hooks fire the instant those components mount, not on any particular
+// click. DisplayPage's empty-project landing (`loadPageWithDefaultBackend`'s default "editor" -
+// see DisplayPage.tsx's own `ImportColumns`) mounts `ImportText` AND `ImportURL` unconditionally
+// (the URL accordion tab is `defaultActiveKey="url"`, open from the start) - so all three fetches
+// already fire on plain page load, same as the already-unskipped `/2/cards`/`/2/sources` tests
+// right above them. No interaction step needed at all - `interactionFn: null`, like those.
test.describe("error reporting toasts", () => {
async function assertErrorToast(
@@ -109,11 +112,7 @@ test.describe("error reporting toasts", () => {
);
});
- test("/2/DFCPairs", async ({ page, network }, testInfo) => {
- testInfo.skip(
- true,
- "Proposal H switchover (2026-07-23): openImportTextModal is classic /editor-only UI, now unrouted - see issue #272"
- );
+ test("/2/DFCPairs", async ({ page, network }) => {
await assertErrorToast(
page,
network,
@@ -125,10 +124,7 @@ test.describe("error reporting toasts", () => {
searchResultsOneResult,
dfcPairsServerError,
],
- async () => {
- // DFC pairs are loaded when an importer is opened
- await openImportTextModal(page);
- }
+ null
);
});
@@ -147,11 +143,7 @@ test.describe("error reporting toasts", () => {
);
});
- test("/2/importSites", async ({ page, network }, testInfo) => {
- testInfo.skip(
- true,
- "Proposal H switchover (2026-07-23): getAddCardsMenu is classic /editor-only UI, now unrouted - see issue #272"
- );
+ test("/2/importSites", async ({ page, network }) => {
await assertErrorToast(
page,
network,
@@ -163,18 +155,11 @@ test.describe("error reporting toasts", () => {
searchResultsOneResult,
importSitesServerError,
],
- async () => {
- const addCardsMenu = getAddCardsMenu(page);
- await addCardsMenu.click();
- }
+ null
);
});
- test("/2/sampleCards", async ({ page, network }, testInfo) => {
- testInfo.skip(
- true,
- "Proposal H switchover (2026-07-23): openImportTextModal is classic /editor-only UI, now unrouted - see issue #272"
- );
+ test("/2/sampleCards", async ({ page, network }) => {
await assertErrorToast(
page,
network,
@@ -186,10 +171,7 @@ test.describe("error reporting toasts", () => {
searchResultsOneResult,
sampleCardsServerError,
],
- async () => {
- // Sample cards are loaded when the text importer is opened
- await openImportTextModal(page);
- }
+ null
);
});
diff --git a/frontend/tests/test-utils.ts b/frontend/tests/test-utils.ts
index 10c29b2d8..9269fe488 100644
--- a/frontend/tests/test-utils.ts
+++ b/frontend/tests/test-utils.ts
@@ -569,6 +569,26 @@ export const openChangeQueryModal = async (
return page.getByTestId("change-query-modal");
};
+// ChangeQueryModal parity port (2026-07-23, issue #272 wave 2). The classic grid opened this
+// modal by clicking a slot's own query text (openChangeQueryModal above); the unified page has
+// no per-slot query text to click - `ChangeQueryModal` itself is unchanged and still mounted
+// globally (Modals.tsx), reachable instead via the shared `CardSlotContextMenu` (right-click a
+// sheet slot -> "Change Query", DisplayPage.tsx's own handleSlotContextMenu - "no new action,
+// just three more ways to reach it on /display" per that file's own comment). Right-clicking
+// reads whichever face is currently displayed (`activeFace`), so a caller wanting the back slot's
+// modal must call `ensureDisplayFace(page, "back")` first, same as `expectDisplaySheetSlotState`.
+export const openDisplayChangeQueryModal = async (page: Page, slot: number) => {
+ await page
+ .getByTestId("page-preview-slot")
+ .nth(slot - 1)
+ .click({ button: "right" });
+ await page
+ .getByTestId("card-slot-context-menu")
+ .getByText("Change Query")
+ .click();
+ return page.getByTestId("change-query-modal");
+};
+
export const changeQueries = async (page: Page, query: string) => {
const textField = page.getByLabel("change-selected-image-queries-text");
await textField.clear();
@@ -707,6 +727,33 @@ export const enableFuzzySearch = async (page: Page) => {
await settingsModal.getByRole("button", { name: "Save Changes" }).click();
};
+// SearchSettings parity port (2026-07-23, issue #272 wave 2). SearchSettings.tsx itself is
+// unchanged and unforked (DisplayPage.tsx's own comment: "relocated here unmodified") - it just
+// lives inside the right rail's Offcanvas (`display-print-settings-rail`) instead of the classic
+// toolbar. Below the `xl` breakpoint that Offcanvas starts closed, reachable only via the gear
+// button (`display-gear-button`, itself `d-xl-none` - hidden at `xl`+, where the rail is already
+// inline instead - playwright.config.ts's own `contextOptions.viewport` override doesn't actually
+// take effect, an unrelated pre-existing quirk: Playwright's real viewport for this whole suite is
+// devices["Desktop Chrome"]'s stock 1280x720, which IS above `xl`, so the gear button is normally
+// hidden and the rail already visible here - conditional click, same `isVisible()`-guard pattern
+// as `openAddCardsDropdown`, keeps this helper correct at either width). Once the rail is open,
+// openSearchSettingsModal's own `getByText(/Search Settings/)` trigger click still works verbatim
+// - no duplicate-heading ambiguity here either (see DisplayPage.tsx's own comment on that button).
+export const openDisplaySearchSettingsModal = async (page: Page) => {
+ const rail = page.getByTestId("display-print-settings-rail");
+ if (!(await rail.isVisible())) {
+ await page.getByTestId("display-gear-button").click();
+ await expect(rail).toBeVisible();
+ }
+ return openSearchSettingsModal(page);
+};
+
+export const enableDisplayFuzzySearch = async (page: Page) => {
+ const settingsModal = await openDisplaySearchSettingsModal(page);
+ await settingsModal.getByText("Precise Search").click();
+ await settingsModal.getByRole("button", { name: "Save Changes" }).click();
+};
+
/**
* Open a StyledDropdownTreeSelect and click an option by its exact label text.
* The container should be the `.react-dropdown-tree-select` element (or a
diff --git a/frontend/tests/visual/InvalidIdentifiersModal.visual.spec.ts b/frontend/tests/visual/InvalidIdentifiersModal.visual.spec.ts
index 33298d6ff..dc8433711 100644
--- a/frontend/tests/visual/InvalidIdentifiersModal.visual.spec.ts
+++ b/frontend/tests/visual/InvalidIdentifiersModal.visual.spec.ts
@@ -11,28 +11,16 @@ import {
import { test } from "../../playwright.setup";
import {
- expectCardSlotToExist,
+ expectDisplaySheetSlotToExist,
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"
- );
-});
-
+// Parity wave 2 (2026-07-23, issue #272): ported onto the unified `/editor` page.
+// InvalidIdentifiersModal.tsx itself is unchanged and unforked, globally mounted (Layout.tsx's
+// ) - reached the same way regardless of route, via InvalidIdentifiersStatus's own
+// "Review Invalid Cards" link (see InvalidIdentifiersStatus.spec.ts's own precedent, this same
+// wave). Only the slot-existence setup checks below needed adapting to the sheet.
test.describe("InvalidIdentifiersModal visual tests", () => {
test("invalid identifiers modal displays the appropriate data", async ({
page,
@@ -50,9 +38,9 @@ test.describe("InvalidIdentifiersModal visual tests", () => {
page,
`2x query 1${SelectedImageSeparator}123\n1 query 2${FaceSeparator}query 3${SelectedImageSeparator}456`
);
- await expectCardSlotToExist(page, 1);
- await expectCardSlotToExist(page, 2);
- await expectCardSlotToExist(page, 3);
+ await expectDisplaySheetSlotToExist(page, 1);
+ await expectDisplaySheetSlotToExist(page, 2);
+ await expectDisplaySheetSlotToExist(page, 3);
// Bring up the modal
const alertText = page.getByText("Your project specified", {
diff --git a/frontend/tests/visual/SearchSettings.visual.spec.ts b/frontend/tests/visual/SearchSettings.visual.spec.ts
index 1f0a3a560..c03ac556e 100644
--- a/frontend/tests/visual/SearchSettings.visual.spec.ts
+++ b/frontend/tests/visual/SearchSettings.visual.spec.ts
@@ -11,29 +11,16 @@ import {
import { test } from "../../playwright.setup";
import {
- expectCardGridSlotState,
- importText,
+ expectDisplaySheetSlotState,
+ importTextOnEditorLanding,
loadPageWithDefaultBackend,
- openSearchSettingsModal,
+ openDisplaySearchSettingsModal,
} 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"
- );
-});
-
+// Parity wave 2 (2026-07-23, issue #272): ported onto the unified `/editor` page.
+// SearchSettings.tsx itself is unchanged and unforked (DisplayPage.tsx's own comment: "the same
+// self-contained trigger-button-plus-modal ProjectEditor.tsx already mounts, relocated here
+// unmodified") - only how it's reached differs (openDisplaySearchSettingsModal, test-utils.ts).
test.describe("SearchSettings visual tests", () => {
test("search settings modal structure", async ({ page, network }) => {
network.use(
@@ -46,10 +33,10 @@ test.describe("SearchSettings visual tests", () => {
await loadPageWithDefaultBackend(page);
// Wait for sources to be fetched by importing a card
- await importText(page, "my search query");
- await expectCardGridSlotState(page, 1, "front", cardDocument1.name, 1, 1);
+ await importTextOnEditorLanding(page, "my search query");
+ await expectDisplaySheetSlotState(page, 1, "front", cardDocument1.name);
- const searchSettings = await openSearchSettingsModal(page);
+ const searchSettings = await openDisplaySearchSettingsModal(page);
await expect(searchSettings.getByText(sourceDocument1.name)).toBeVisible();
// Wait until all spinners have finished loading