From 5fd368585aa1bf0818db0194848e87bdea5d9006 Mon Sep 17 00:00:00 2001 From: dborup Date: Thu, 1 Oct 2026 06:52:05 +0200 Subject: [PATCH 1/2] test(e2e): reproduce the fluid-charts flake with a held-back stylesheet (#148) The harness navigates with waitUntil 'domcontentloaded', which does not wait for style.css. Route style.css through the context so it can be held back, and add a step that holds it back for a second before measuring the 1300px case. Unstyled cards stack in one column, so the step fails with "got 1", the same message as the CI flakes. It fails 10/10 locally until the measurement waits for the stylesheet. FLUID_CHARTS_CSS_DELAY_MS holds style.css back on every load, which reproduces the failing steps seen in CI (2520px side-by-side, AC3 precondition). Co-Authored-By: Claude Opus 5.5 --- test-analytics-fluid-charts.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test-analytics-fluid-charts.js b/test-analytics-fluid-charts.js index b160700b1..40705e4b4 100644 --- a/test-analytics-fluid-charts.js +++ b/test-analytics-fluid-charts.js @@ -17,6 +17,9 @@ * Pure file:// harness — does not require the Go server. * * Usage: node test-analytics-fluid-charts.js + * + * FLUID_CHARTS_CSS_DELAY_MS= holds style.css back that long on every + * load, as a slow runner would (#148). */ 'use strict'; const { chromium } = require('playwright'); @@ -69,6 +72,14 @@ function assert(c, m) { if (!c) throw new Error(m || 'assertion failed'); } args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'], }); const ctx = await browser.newContext(); + // #148: on a slow runner style.css can apply after DOMContentLoaded. + // cssDelayMs holds it back that long, so a test can reproduce that. + const envCssDelayMs = Number(process.env.FLUID_CHARTS_CSS_DELAY_MS) || 0; + let cssDelayMs = envCssDelayMs; + await ctx.route((url) => url.href === cssHref, async (route) => { + if (cssDelayMs > 0) await new Promise((r) => setTimeout(r, cssDelayMs)); + await route.continue(); + }); const page = await ctx.newPage(); page.on('pageerror', (e) => console.error('[pageerror]', e.message)); @@ -120,6 +131,21 @@ function assert(c, m) { if (!c) throw new Error(m || 'assertion failed'); } `horizontal overflow: scrollW=${o.scrollW} clientW=${o.clientW}`); }); + // --- #148: measure only once style.css applies ------------------------ + // Same case as above with style.css held back for a second. Unstyled + // cards stack in one column, so a measurement taken before the + // stylesheet applies reads 1 column. + await step('viewport 1440 / wrapper 1300px with style.css held back 1s → side-by-side (≥2 cols)', async () => { + cssDelayMs = Math.max(envCssDelayMs, 1000); + try { + await load(1300, 1440); + } finally { + cssDelayMs = envCssDelayMs; + } + const cols = await colCount(); + assert(cols >= 2, `expected ≥2 columns at wrapper 1300px; got ${cols}`); + }); + // --- Viewport 1080: medium width — must not overflow ------------------ await step('viewport 1080 / wrapper 1040px → no horizontal overflow', async () => { await load(1040, 1080); From 26b934e90a2fbdf84cb31d528df5e6a8f2104428 Mon Sep 17 00:00:00 2001 From: dborup Date: Thu, 1 Oct 2026 06:53:11 +0200 Subject: [PATCH 2/2] test(e2e): measure fluid charts only once style.css applies (#148) Navigate with waitUntil 'load' and wait until the harness's stylesheet link has its sheet, then two frames, before measuring. The AC3 resize step uses the same wait after setViewportSize. The wait is on the stylesheet itself, not on container-type or the grid template, so a regression in the CSS under test still fails on its own assertion. Assertions (column counts, overflow, reflow) are unchanged. Co-Authored-By: Claude Opus 5.5 --- test-analytics-fluid-charts.js | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/test-analytics-fluid-charts.js b/test-analytics-fluid-charts.js index 40705e4b4..25d7ddd44 100644 --- a/test-analytics-fluid-charts.js +++ b/test-analytics-fluid-charts.js @@ -90,7 +90,24 @@ function assert(c, m) { if (!c) throw new Error(m || 'assertion failed'); } const tmp = path.join(os.tmpdir(), `1058-harness-${wrapperWidth}-${viewportWidth}.html`); fs.writeFileSync(tmp, harnessHTML(wrapperWidth)); - await page.goto('file://' + tmp, { waitUntil: 'domcontentloaded' }); + await page.goto('file://' + tmp, { waitUntil: 'load' }); + await waitForStyles(); + } + + // #148: DOMContentLoaded does not wait for style.css, and unstyled + // cards stack in one column. Wait until the stylesheet is attached, + // then for two frames so layout reflects it, before measuring. This + // waits on the stylesheet itself, not on the properties under test. + async function waitForStyles() { + try { + await page.waitForFunction(() => { + const link = document.querySelector('link[rel="stylesheet"]'); + return !!(link && link.sheet); + }, null, { timeout: 15000 }); + } catch (e) { + throw new Error('style.css was not applied: ' + e.message); + } + await page.evaluate(() => new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)))); } // Helper: count distinct column-x-positions of chart cards. @@ -214,8 +231,9 @@ function assert(c, m) { if (!c) throw new Error(m || 'assertion failed'); } document.getElementById('wrap').style.width = '760px'; }); await page.setViewportSize({ width: 768, height: 900 }); - // Give the browser a frame to recompute layout. - await page.evaluate(() => new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)))); + // Same wait as after a load: styles applied, then two frames so the + // layout is recomputed for the new width. + await waitForStyles(); const colsNarrow = await colCount(); assert(colsNarrow === 1, `expected layout to reflow to 1 column after shrink; got ${colsNarrow}`);