From c941e7e5cb68e9210aaea67933058e0cb1d5d1a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 26 Jul 2026 00:38:49 +0200 Subject: [PATCH] test(e2e): measure the heading caption atomically MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wrap assertion gated on the section heading, but the signals card paints a skeleton heading first and swaps it for the resolved one — so the element could detach between the visibility check and the measurement, and the skeleton heading carries no action slot to measure in the first place. Gate on the caption itself and read both rects in a single evaluate. --- e2e/mobile-horizontal-overflow.spec.ts | 39 +++++++++++++++++--------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/e2e/mobile-horizontal-overflow.spec.ts b/e2e/mobile-horizontal-overflow.spec.ts index 857209a20..e4f0758a4 100644 --- a/e2e/mobile-horizontal-overflow.spec.ts +++ b/e2e/mobile-horizontal-overflow.spec.ts @@ -130,27 +130,40 @@ test.describe("content-heavy routes have no horizontal page scroll at phone widt await page.setViewportSize({ width: 390, height: 851 }); await page.goto("/insights", { waitUntil: "domcontentloaded" }); - const heading = page + // Wait on the CAPTION, not the heading: the card first paints a skeleton + // whose heading carries no action slot, then swaps to the resolved shell. + // Both render `[data-slot="section-heading"]`, so gating on the heading + // alone can measure the skeleton — or measure nothing at all, because the + // element detaches during the swap. + const method = page .locator( - '[data-slot="coincident-deviation-section"] [data-slot="section-heading"]', + '[data-slot="coincident-deviation-section"] [data-slot="provenance-explainer-method"]', ) .first(); - await expect(heading).toBeVisible(); + await expect(method).toBeVisible(); - const box = await heading.boundingBox(); - const method = heading.locator('[data-slot="provenance-explainer-method"]'); - const methodBox = await method.boundingBox(); - expect(box, "heading has no box").not.toBeNull(); - expect(methodBox, "provenance caption has no box").not.toBeNull(); + // Read both rects in ONE evaluate so the swap cannot land between them. + const box = await method.evaluate((el) => { + const headingEl = el.closest('[data-slot="section-heading"]'); + if (!headingEl) return null; + const caption = el.getBoundingClientRect(); + const heading = headingEl.getBoundingClientRect(); + return { + captionWidth: caption.width, + headingWidth: heading.width, + viewportWidth: window.innerWidth, + }; + }); + expect(box, "caption is not inside a section heading").not.toBeNull(); expect( - Math.round(methodBox!.width), - `caption width ${methodBox!.width} exceeds its heading row ${box!.width} — the action slot is sizing to max-content instead of wrapping`, - ).toBeLessThanOrEqual(Math.round(box!.width) + 1); + Math.round(box!.captionWidth), + `caption width ${box!.captionWidth} exceeds its heading row ${box!.headingWidth} — the action slot is sizing to max-content instead of wrapping`, + ).toBeLessThanOrEqual(Math.round(box!.headingWidth) + 1); expect( - Math.round(box!.width), + Math.round(box!.headingWidth), "heading row is wider than the viewport", - ).toBeLessThanOrEqual(390); + ).toBeLessThanOrEqual(box!.viewportWidth); }); /**