From 79ac9a96214661f73b7b5db6582c2b508560abbe Mon Sep 17 00:00:00 2001 From: Steve Fackley Date: Mon, 22 Jun 2026 14:39:11 -0400 Subject: [PATCH 1/3] fix(web): use Playwright getByPlaceholder in advanced-draft smoke (repairs red main CI) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit advanced-draft-persistence.spec.ts called page.getByDisplayValue(), a Testing-Library API that does not exist on Playwright's Page — every smoke run threw "TypeError: page.getByDisplayValue is not a function", failing the E2E Smoke (Demo Mode) + Quality Gate jobs and reddening main since 93cb6b25 (#142). Locate the single entity-name input by its placeholder ("EntityName") and assert state with toHaveValue, matching how the RTL component test already queries it. Unblocks Dependabot #169/#170. --- .../e2e/smoke/advanced-draft-persistence.spec.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/StackAlchemist.Web/e2e/smoke/advanced-draft-persistence.spec.ts b/src/StackAlchemist.Web/e2e/smoke/advanced-draft-persistence.spec.ts index 07e6966..dc0389d 100644 --- a/src/StackAlchemist.Web/e2e/smoke/advanced-draft-persistence.spec.ts +++ b/src/StackAlchemist.Web/e2e/smoke/advanced-draft-persistence.spec.ts @@ -9,27 +9,28 @@ test.describe("Smoke: Advanced Draft Persistence", () => { test("wizard edits survive a reload and show the restore notice", async ({ page }) => { await page.goto("/advanced?step=1"); - const entityName = page.getByDisplayValue("Product"); + const entityName = page.getByPlaceholder("EntityName"); + await expect(entityName).toHaveValue("Product"); await entityName.fill("Subscription"); // Outlive the 800ms persist debounce before reloading. await page.waitForTimeout(1200); await page.reload(); - await expect(page.getByDisplayValue("Subscription")).toBeVisible(); + await expect(page.getByPlaceholder("EntityName")).toHaveValue("Subscription"); await expect(page.getByTestId("advanced-draft-restored")).toBeVisible(); }); test("start fresh discards the draft", async ({ page }) => { await page.goto("/advanced?step=1"); - await page.getByDisplayValue("Product").fill("Subscription"); + await page.getByPlaceholder("EntityName").fill("Subscription"); await page.waitForTimeout(1200); await page.reload(); await expect(page.getByTestId("advanced-draft-restored")).toBeVisible(); await page.getByRole("button", { name: "start fresh" }).click(); - await expect(page.getByDisplayValue("Product")).toBeVisible(); + await expect(page.getByPlaceholder("EntityName")).toHaveValue("Product"); await page.reload(); await expect(page.getByTestId("advanced-draft-restored")).not.toBeVisible(); }); From 34c5e98fed3ec5be6abdb2417f405e134125872d Mon Sep 17 00:00:00 2001 From: Steve Fackley Date: Mon, 22 Jun 2026 14:49:08 -0400 Subject: [PATCH 2/3] fix(web): clear smoke draft storage once, not on every reload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit beforeEach used page.addInitScript(() => localStorage.clear()), which re-runs on EVERY navigation — including the page.reload() the persistence tests depend on — wiping the saved draft mid-test. So even after the getByDisplayValue fix, the "survives a reload" assertion could never pass (input reverted to default). Clear storage once after landing on the origin instead. Verified locally against a demo-mode server: all 3 advanced-draft smoke tests pass. --- .../e2e/smoke/advanced-draft-persistence.spec.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/StackAlchemist.Web/e2e/smoke/advanced-draft-persistence.spec.ts b/src/StackAlchemist.Web/e2e/smoke/advanced-draft-persistence.spec.ts index dc0389d..027b3ee 100644 --- a/src/StackAlchemist.Web/e2e/smoke/advanced-draft-persistence.spec.ts +++ b/src/StackAlchemist.Web/e2e/smoke/advanced-draft-persistence.spec.ts @@ -2,8 +2,11 @@ import { expect, test } from "@playwright/test"; test.describe("Smoke: Advanced Draft Persistence", () => { test.beforeEach(async ({ page }) => { - // Drafts from prior specs/sessions must not leak in. - await page.addInitScript(() => window.localStorage.clear()); + // Drafts from prior specs/sessions must not leak in. Clear storage ONCE — + // NOT via page.addInitScript(), which re-runs on every navigation including + // the page.reload() these tests depend on, wiping the draft mid-test. + await page.goto("/advanced"); + await page.evaluate(() => window.localStorage.clear()); }); test("wizard edits survive a reload and show the restore notice", async ({ page }) => { From a7bb48d03754e09b5648c9334b81d11ff0d65a55 Mon Sep 17 00:00:00 2001 From: Steve Fackley Date: Mon, 22 Jun 2026 15:00:15 -0400 Subject: [PATCH 3/3] fix(web): assert focus-trap by containment, not innerText text match MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Tab stays trapped" smoke test compared the focused element's innerText (CSS text-transform: uppercase -> "✦ HELP ME WRITE THIS") against the dialog's textContent (raw "✦ Help me write this") via toContainText, which is case- sensitive — so it never matched once focus reached the uppercased CTA button. Assert dialog.contains(activeElement) directly, which is what the focus trap actually guarantees. Verified locally: all personalization + advanced-draft smoke tests pass. --- .../smoke/personalization-modal-keyboard.spec.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/StackAlchemist.Web/e2e/smoke/personalization-modal-keyboard.spec.ts b/src/StackAlchemist.Web/e2e/smoke/personalization-modal-keyboard.spec.ts index d5def60..b8a9bf8 100644 --- a/src/StackAlchemist.Web/e2e/smoke/personalization-modal-keyboard.spec.ts +++ b/src/StackAlchemist.Web/e2e/smoke/personalization-modal-keyboard.spec.ts @@ -19,12 +19,17 @@ test.describe("Smoke: Personalization modal keyboard a11y", () => { await page.getByTestId("advanced-personalize-button").click(); await expect(page.getByRole("dialog")).toBeVisible(); - // Tab through all focusable elements — focus must not escape to the page behind. + // Tab through focusable elements — focus must stay trapped inside the dialog, + // not escape to the page behind. Assert containment directly: comparing the + // focused element's innerText (CSS-uppercased) against the dialog's textContent + // (raw, mixed-case) was brittle and never matched — the original bug. for (let i = 0; i < 10; i++) { await page.keyboard.press("Tab"); - const focused = page.locator(":focus"); - const dialogHandle = page.getByRole("dialog"); - await expect(dialogHandle).toContainText(await focused.innerText().catch(() => "")); + const trapped = await page.evaluate(() => { + const dialog = document.querySelector('[role="dialog"]'); + return !!dialog && dialog.contains(document.activeElement); + }); + expect(trapped, `focus escaped the dialog after Tab #${i + 1}`).toBe(true); } // Confirm: after Escape, some element on the page (opener button) receives focus.