From a47f67b3dc5be2da382b56d1d9ff2d86ce301e84 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 16:10:17 +0000 Subject: [PATCH 1/3] test(webkit): wait for the settled copy after a streamed reload Main's Safari matrix after #3497 cleared the DSM compare, On Call anchor and hidden-tabs checks. Two new reads failed on the same cause: the test looked while a streamed page was still swapping its content in. - Phone assessments: after the reload the hero existed twice for a moment, which a strict locator rejects without retrying. Wait for one copy first. - Forms home hero: the icon was visible, then its box read came back empty because the subtree was replaced in between. Poll the box. No test skipped or loosened; no app code changed. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01DCghuougzFFXRF3dQQEcYr --- tests/ui-phone-scroll-assessments.spec.ts | 3 +++ tests/ui-tools.spec.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/tests/ui-phone-scroll-assessments.spec.ts b/tests/ui-phone-scroll-assessments.spec.ts index 19f57c0bfd..06fd97c30a 100644 --- a/tests/ui-phone-scroll-assessments.spec.ts +++ b/tests/ui-phone-scroll-assessments.spec.ts @@ -89,6 +89,9 @@ for (const standalone of [false, true]) { await expect(page).toHaveURL(/view=progress/); await expect(page.locator("[data-screen-heading]").first()).toBeFocused(); await page.goto("/teaching/assessments"); + // A streamed page can briefly hold a finished section twice (once still parked in React's hidden + // reveal container), which a strict locator rejects at once. Wait for the single settled copy. + await expect(page.getByTestId("assess-doctor-hero")).toHaveCount(1, { timeout: 15_000 }); await expect(page.getByTestId("assess-doctor-hero")).toBeVisible(); await page.getByRole("button", { name: /^Request an EPA From/ }).click(); await expect(page.getByRole("dialog")).toBeVisible(); diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index 1cdffa5f18..595e4e77d0 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -1241,6 +1241,9 @@ test.describe("PsychSift tools directory and legacy launcher", () => { const icon = homeRegion.locator(".mode-home-icon").first(); await expect(icon).toBeVisible(); + // The home can swap its subtree between the visibility check and a single box read, so read the + // box until the settled icon answers. + await expect.poll(() => icon.boundingBox(), { message: `${home.path} hero icon` }).not.toBeNull(); const iconBox = await icon.boundingBox(); expect(iconBox, `${home.path} hero icon`).not.toBeNull(); From 6ea9c87c0ee4ea6742e5dd4735777da539883eb3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 16:15:32 +0000 Subject: [PATCH 2/3] test: keep the icon box from the poll read that succeeded Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01DCghuougzFFXRF3dQQEcYr --- tests/ui-tools.spec.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index 595e4e77d0..f7f806f81a 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -1242,9 +1242,12 @@ test.describe("PsychSift tools directory and legacy launcher", () => { const icon = homeRegion.locator(".mode-home-icon").first(); await expect(icon).toBeVisible(); // The home can swap its subtree between the visibility check and a single box read, so read the - // box until the settled icon answers. - await expect.poll(() => icon.boundingBox(), { message: `${home.path} hero icon` }).not.toBeNull(); - const iconBox = await icon.boundingBox(); + // box until the settled icon answers, and keep the read that did. + const read: { box: Awaited> } = { box: null }; + await expect + .poll(async () => (read.box = await icon.boundingBox()), { message: `${home.path} hero icon` }) + .not.toBeNull(); + const iconBox = read.box; expect(iconBox, `${home.path} hero icon`).not.toBeNull(); // ModeHomeHero gives its heading the deterministic id `-title` From 562d0eaff0e33f11f35870e28f7e93768bbe840e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 16:19:54 +0000 Subject: [PATCH 3/3] test: check the settled hero icon size inside one fresh poll Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01DCghuougzFFXRF3dQQEcYr --- tests/ui-tools.spec.ts | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index f7f806f81a..6411d66151 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -1241,14 +1241,18 @@ test.describe("PsychSift tools directory and legacy launcher", () => { const icon = homeRegion.locator(".mode-home-icon").first(); await expect(icon).toBeVisible(); - // The home can swap its subtree between the visibility check and a single box read, so read the - // box until the settled icon answers, and keep the read that did. - const read: { box: Awaited> } = { box: null }; + // The hero tile is `h-tap w-tap` on phones, so it tracks `--spacing-tap` + // (48px since PR 5b), not a standalone pixel choice. The home can swap its subtree between the + // visibility check and a box read, so each sample is a fresh read of the settled icon. await expect - .poll(async () => (read.box = await icon.boundingBox()), { message: `${home.path} hero icon` }) - .not.toBeNull(); - const iconBox = read.box; - expect(iconBox, `${home.path} hero icon`).not.toBeNull(); + .poll( + async () => { + const box = await icon.boundingBox(); + return box && { width: Math.round(box.width), height: Math.round(box.height) }; + }, + { message: `${home.path} hero icon size` }, + ) + .toEqual({ width: 48, height: 48 }); // ModeHomeHero gives its heading the deterministic id `-title` // (role/name lookups can collide with sr-only section headings). @@ -1277,16 +1281,7 @@ test.describe("PsychSift tools directory and legacy launcher", () => { }; }); - const metrics = { - iconWidth: Math.round(iconBox?.width ?? 0), - iconHeight: Math.round(iconBox?.height ?? 0), - headingFontSize, - }; - // The hero tile is `h-tap w-tap` on phones, so it tracks `--spacing-tap` - // (48px since PR 5b), not a standalone pixel choice. - expect(metrics.iconWidth).toBe(48); - expect(metrics.iconHeight).toBe(48); - expect(metrics.headingFontSize).toBeCloseTo(expectedTypeSizes.hero, 1); + expect(headingFontSize).toBeCloseTo(expectedTypeSizes.hero, 1); expect(subtitleFontSize).toBeCloseTo(expectedTypeSizes.subtitle, 1); await expectNoPageHorizontalOverflow(page);