diff --git a/apps/web/tests/conversation-column-overflow.e2e.ts b/apps/web/tests/conversation-column-overflow.e2e.ts index 5744986598..03ba4f9572 100644 --- a/apps/web/tests/conversation-column-overflow.e2e.ts +++ b/apps/web/tests/conversation-column-overflow.e2e.ts @@ -49,6 +49,8 @@ const MODE = webSnapshotMode() const WIDTHS = [1680, 1200, 1000, 800, 600] /** Element id of the mutation control's injected sheet, so the test can take it back out. */ const CONTROL_STYLE_ID = 'dsh-column-overflow-control' +/** Horizontal wheel delta per gesture; must exceed the widest bleed the sweep can produce. */ +const WHEEL_DELTA = 300 /** One viewport stop: whether the glow bleeds past the column, and whether that bleed scrolls. */ interface ColumnMetrics { @@ -124,7 +126,7 @@ async function wheelHorizontally(page: Page): Promise { return { x: box.left + box.width / 2, y: box.top + 60 } }) await page.mouse.move(origin.x, origin.y) - await page.mouse.wheel(300, 0) + await page.mouse.wheel(WHEEL_DELTA, 0) // A fixed settle, then two frames. Polling for a settled value cannot be // used here — the value under test is 0, which a poll starting at 0 accepts // before the gesture has had any chance to move it — so the wait is @@ -190,28 +192,45 @@ describe('web e2e: the conversation column scrolls on one axis', () => { }) /** - * Sweep the stops once and hand the readings to every assertion below, so - * the golden and the assertions describe the same measurement rather than - * two runs that could disagree. + * Resize to a viewport and read the column once its width stops moving. + * + * The glow rides the hero box, which rides the column, and the frame eases + * its column tracks over `--ds-transition-duration-slow`: reading straight + * after a resize can report the previous viewport's relation, or a width + * caught mid-transition. + * @param width - viewport width to settle at. + * @returns the column's readings at that width. + */ + const settleAt = async (width: number): Promise => { + await page.setViewportSize({ width, height: 900 }) + let previous = -1 + await expect.poll(async () => { + const current = (await measureColumn(page, width)).columnWidth + const settled = current === previous + previous = current + return settled + }, { timeout: 10_000 }).toBe(true) + return measureColumn(page, width) + } + + /** + * Sweep the stops once per run and hand the SAME readings to every assertion + * below, so the golden and the assertions describe one measurement instead of + * two runs that could disagree. Memoized rather than re-run per test: the + * gestures below move the viewport, and a second sweep would be a second + * chance for a resize to settle differently. * @returns the stops in {@link WIDTHS} order. */ - const sweep = async (): Promise => { - const stops: ColumnStop[] = [] - for (const width of WIDTHS) { - await page.setViewportSize({ width, height: 900 }) - // The glow rides the hero box, which rides the column, and the column's - // track animates: settle on a column width that stops moving, or a stop - // gets read mid-transition and reports the previous viewport's relation. - let previous = -1 - await expect.poll(async () => { - const current = (await measureColumn(page, width)).columnWidth - const settled = current === previous - previous = current - return settled - }, { timeout: 10_000 }).toBe(true) - stops.push({ ...await measureColumn(page, width), scrollLeftAfterWheel: await wheelHorizontally(page) }) - } - return stops + let swept: Promise | undefined + const sweep = (): Promise => { + swept ??= (async () => { + const stops: ColumnStop[] = [] + for (const width of WIDTHS) { + stops.push({ ...await settleAt(width), scrollLeftAfterWheel: await wheelHorizontally(page) }) + } + return stops + })() + return swept } it('never scrolls horizontally, at any width the glow bleeds past', async () => { @@ -242,7 +261,10 @@ describe('web e2e: the conversation column scrolls on one axis', () => { // that a one-axis scroller computes to `auto` — and shows the same gesture, // at the same timing, carrying the column to the full bleed. Without it a // `scrollLeft` of 0 could equally mean the wheel never arrived. - await page.setViewportSize({ width: 1200, height: 900 }) + // Settle the resize first: this test runs at 1680 on its own and after the + // sweep's 600 in a full run, and an unsettled column reports the previous + // viewport's bleed. + await settleAt(1200) // Injected with an id rather than through `addStyleTag`, so the teardown // below can take the sheet out again by selector: it must not outlive this // test, or the golden ends up reading the control. @@ -255,8 +277,16 @@ describe('web e2e: the conversation column scrolls on one axis', () => { try { const before = await measureColumn(page, 1200) expect(before.overflowX).toBe('auto') - expect(await wheelHorizontally(page)).toBe(before.bleedRange) expect(before.bleedRange).toBeGreaterThan(0) + // The gesture has to be able to reach the far edge, or the equality below + // would fail on the clamp and read as a broken fix. Stated as its own + // assertion so that failure names itself. + expect(before.bleedRange).toBeLessThan(WHEEL_DELTA) + // Rounded: `scrollLeft` is fractional under a fractional layout while + // `scrollWidth - clientWidth` is integral, and the claim is that the + // column travelled the whole bleed — not that two engines agree on a + // sub-pixel. + expect(Math.round(await wheelHorizontally(page))).toBe(before.bleedRange) } finally { await page.evaluate((id: string) => { document.getElementById(id)?.remove()