From 3b948d68ea802cfc406b716cc0dd79ed2c4c0423 Mon Sep 17 00:00:00 2001 From: Sergey Kozyrenko Date: Fri, 24 Jul 2026 13:00:06 +0700 Subject: [PATCH] test(e2e): prove the pager route by its history trail, not by sampling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sampling window.location on a timer measured the host's speed: on an unloaded machine the sibling landed before the third sample, so the guard that the samples spanned the switch failed 3 runs in 4. Weakening it to a length check made it vacuous instead — the sampling loop always runs its full count. Record every pushState/replaceState the app makes and assert the exact sequence, which is what "without passing through the list" claims. Verified by routing the pager through the list: the trail assert names the detour. Co-Authored-By: Claude Opus 4.8 --- frontend/e2e/specs/flows/pager.spec.ts | 48 +++++++++++++------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/frontend/e2e/specs/flows/pager.spec.ts b/frontend/e2e/specs/flows/pager.spec.ts index 10e0a9d3..1ba32da8 100644 --- a/frontend/e2e/specs/flows/pager.spec.ts +++ b/frontend/e2e/specs/flows/pager.spec.ts @@ -32,43 +32,43 @@ test.describe('flow pager', { tag: ['@flows', '@smoke'] }, () => { test('steps to the sibling flow and back without passing through the list', async ({ page, pageErrorLog }) => { const header = page.locator('header'); + await page.addInitScript(() => { + const trail: string[] = []; + + (window as unknown as { __routeTrail: string[] }).__routeTrail = trail; + + for (const method of ['pushState', 'replaceState'] as const) { + const original = history[method].bind(history); + + history[method] = (state: unknown, unused: string, url?: null | string | URL) => { + original(state, unused, url); + trail.push(window.location.pathname); + }; + } + }); + await page.goto('/flows/5'); await expect(header.getByText('E2E Alpha')).toBeVisible(); - await page.route('**/graphql', async (route) => { - await new Promise((resolve) => setTimeout(resolve, 300)); - await route.fallback(); + // Drop the router's own normalising replaceState from the initial load. + await page.evaluate(() => { + (window as unknown as { __routeTrail: string[] }).__routeTrail.length = 0; }); await header.getByRole('button', { name: 'Next' }).click(); - const samples = await page.evaluate(async () => { - const taken = []; - - for (let index = 0; index < 10; index += 1) { - taken.push({ - hasPager: !!document.querySelector('header button[aria-label="Next"]'), - isSiblingShown: document.querySelector('header')?.textContent?.includes('E2E Beta') ?? false, - path: window.location.pathname, - }); - await new Promise((resolve) => setTimeout(resolve, 30)); - } - - return taken; - }); - await expect(page).toHaveURL(/\/flows\/6$/); await expect(header.getByText('E2E Beta')).toBeVisible(); - - // Guards the delay above as much as the pager: without it the sibling lands inside the - // first sample and the loop measures nothing. - expect(samples.filter((sample) => !sample.isSiblingShown).length).toBeGreaterThan(2); - expect(samples.every((sample) => sample.hasPager)).toBe(true); - expect(samples.map((sample) => sample.path)).not.toContain('/flows'); + await expect(header.getByRole('button', { name: 'Next' })).toBeVisible(); await header.getByRole('button', { name: 'Previous' }).click(); await expect(page).toHaveURL(/\/flows\/5$/); + await expect(header.getByText('E2E Alpha')).toBeVisible(); + + const trail = await page.evaluate(() => (window as unknown as { __routeTrail: string[] }).__routeTrail); + + expect(trail, 'the pager must step straight between siblings').toEqual(['/flows/6', '/flows/5']); expectCleanPage(pageErrorLog); }); });