From 25f1e2ff82c71b46bd25a7c8f195b6ce27b8134f Mon Sep 17 00:00:00 2001 From: Kristian Wind Date: Mon, 17 Aug 2026 10:48:38 +0200 Subject: [PATCH] fix(e2e): wait for the sidebar before counting its links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The suite passed on a laptop and failed on the runner with zero links found. locator.all() is a snapshot and does not auto-wait: calling it straight after goto() measures how fast the machine is, not what the page contains. An assertion on the first link waits for hydration, and the hrefs are collected once and re-resolved on each pass rather than held as element handles — navigating re-renders the sidebar, so a handle from before the click points at an element that no longer exists. Co-Authored-By: Claude Opus 5 --- web/e2e/smoke.spec.js | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/web/e2e/smoke.spec.js b/web/e2e/smoke.spec.js index eff4d9e..a3d9565 100644 --- a/web/e2e/smoke.spec.js +++ b/web/e2e/smoke.spec.js @@ -64,12 +64,23 @@ test('hvert link i sidebjælken fører et sted hen', async ({ page }) => { await page.goto('/'); const sidebar = page.getByRole('navigation', { name: 'Hovedmenu' }); - const links = await sidebar.getByRole('link').all(); - expect(links.length).toBeGreaterThanOrEqual(4); // I dag, Kommende, Indbakke, Indstillinger - for (const link of links) { - const href = await link.getAttribute('href'); - await link.click(); + // Wait for one link before counting them. `all()` is a snapshot with no + // auto-waiting — it returns whatever matches at that instant — so counting + // straight after goto() measures how fast the machine is. It passed on a + // laptop and found zero links on a CI runner. + await expect(sidebar.getByRole('link').first()).toBeVisible(); + + const hrefs = await sidebar + .getByRole('link') + .evaluateAll((links) => links.map((a) => a.getAttribute('href'))); + expect(hrefs.length).toBeGreaterThanOrEqual(4); // I dag, Kommende, Indbakke, Indstillinger + + // Re-resolved by href on each pass rather than held as handles: navigating + // re-renders the sidebar, and a handle from before the click is a handle to + // an element that no longer exists. + for (const href of hrefs) { + await sidebar.locator(`a[href="${href}"]`).first().click(); // Something with a heading rendered. A route written outside the routes // tree lands on the SPA shell and renders nothing — which is exactly how