From 55dd28164dfdb2a61164136c66cd0bbe49bdafce Mon Sep 17 00:00:00 2001 From: efiten Date: Sun, 11 Oct 2026 10:58:20 +0200 Subject: [PATCH] test(e2e): assert the configured-scope row after the theme-refresh re-render (#2180) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What `test-issue-1648-m2-icons-e2e.js` now asserts the configured-scope row only after the page's `theme-refresh` re-render has fired and the detail has rendered again. ## Why On 2026-10-10 this assertion failed CI on four PRs (#2169, #2171, #2154, #2164), each time at the first live-map case: `confirmed scope row must be visible, including an empty scope`. A re-run of the failed jobs passed every time. The code path: - `customize-v2.js:814` dispatches `theme-changed` once `/api/config/theme` resolves, on every page load. - `app.js:1309-1315` turns that into `theme-refresh` 300 ms later. - `live.js:4713` re-runs `showNodeDetail(activeNodeDetailKey)` on `theme-refresh`, and `showNodeDetail` first sets `#nodeDetailContent` to "Loading…" (`live.js:2598`). This is the only caller besides the marker click (`live.js:3090`). - The test waited for `#nodeDetailContent table` and then called `row.isVisible()`, which does not wait. A refresh landing between the two fails the check. The same race could let the null-scope case pass on the "Loading…" panel, because `row.count() === 0` holds there too. The fix uses the same `addInitScript` flag pattern as `test-e2e-playwright.js:83`. ## Testing - Locally against a server on the E2E fixture: 5 of 5 runs pass, 27 of 27 checks each. - **Not reproduced locally.** The unchanged test also passed every local run: 5 plain, 3 with `/api/config/theme` delayed by 1 s, 5 with 8x CPU throttling. So the reasoning above comes from the code path and the four CI failures, not from a local red-then-green. - Test-only change, no product code touched. Co-authored-by: Claude Opus 5.5 --- tests/e2e/test-issue-1648-m2-icons-e2e.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/e2e/test-issue-1648-m2-icons-e2e.js b/tests/e2e/test-issue-1648-m2-icons-e2e.js index c31ce206..a7ee7d66 100644 --- a/tests/e2e/test-issue-1648-m2-icons-e2e.js +++ b/tests/e2e/test-issue-1648-m2-icons-e2e.js @@ -86,6 +86,12 @@ async function checkConfiguredScope(page) { await page.route(nodeListUrl, route => route.fulfill({ json: { nodes: [{ ...node, lat: 1, lon: 1 }], total: 1 }, })); + // Every page load fires one 'theme-refresh' about 300ms after the theme + // loads, and live.js re-runs showNodeDetail on it, which briefly swaps the + // detail panel to "Loading…". Assert only after that re-render. + await page.addInitScript(() => { + window.addEventListener('theme-refresh', () => { window.__scopeThemeReady = true; }, { once: true }); + }); try { for (const live of [false, true]) { for (const scope of ['#fixture', '', null]) { @@ -95,7 +101,10 @@ async function checkConfiguredScope(page) { const route = live ? '/live?lat=1&lon=1&zoom=10' : '/nodes/' + key; await page.goto(`${BASE}/#${route}`, { waitUntil: 'domcontentloaded' }); if (live) await page.locator('#liveMap .live-node-marker').click(); - await page.locator(live ? '#nodeDetailContent table' : '#node-stats').waitFor({ state: 'visible', timeout: 8000 }); + const detail = page.locator(live ? '#nodeDetailContent table' : '#node-stats'); + await detail.waitFor({ state: 'visible', timeout: 8000 }); + await page.waitForFunction(() => window.__scopeThemeReady === true, null, { timeout: 8000 }); + await detail.waitFor({ state: 'visible', timeout: 8000 }); const row = live ? page.locator('#nodeDetailContent tr').filter({ hasText: 'Configured scope' }) : page.locator('#row-configured-scope');