From bdcfb33b587be94f910b06d52782f78fe91f3345 Mon Sep 17 00:00:00 2001 From: Joan Leon Date: Wed, 21 Oct 2026 19:31:23 +0200 Subject: [PATCH] test(third-party-impact): wait for the long frame before reading it, and say what the page saw The test of the warning for a domain with a large share of main-thread time failed twice in CI with 0 ms for the blocking script, and never on a laptop or in Docker with Playwright 1.63, two CPUs and the whole e2e suite running at the same time (60 loads and 30 runs). Either the long animation frame reached the timeline after the fixed wait of 500 ms, or it came without the script. The test now waits up to 5 seconds for a long animation frame instead of a fixed time, which covers the first case. If it fails again, its message says how long it waited and lists the frames of the page with their scripts, which tells the two cases apart. --- cli/tests/e2e/third-party-impact.test.js | 32 ++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/cli/tests/e2e/third-party-impact.test.js b/cli/tests/e2e/third-party-impact.test.js index 3aee1563..1d32f7fe 100644 --- a/cli/tests/e2e/third-party-impact.test.js +++ b/cli/tests/e2e/third-party-impact.test.js @@ -107,6 +107,34 @@ async function run(route, { withoutLoaf = false, frames } = {}) { } } +// A script that blocks while the page parses makes a long animation frame, and on a busy machine +// that frame can reach the performance timeline after the fixed wait of run(): CI failed twice with +// 0 ms for the script, which never happened on a laptop or in Docker. This waits up to 5 s for the +// frame, and returns the frames the page saw, so a failure says whether the frame was missing or +// came without the script. +async function runAfterLongFrame(route) { + const browser = await chromium.launch(); + try { + const page = await browser.newPage(); + await page.goto(`${pageBase}${route}`, { waitUntil: "load" }); + const start = Date.now(); + await page + .waitForFunction(() => performance.getEntriesByType("long-animation-frame").length > 0, null, { timeout: 5000 }) + .catch(() => {}); + const waitedMs = Date.now() - start; + const result = await page.evaluate(asExpression("Loading/Third-Party-Impact-by-Domain")); + const frames = await page.evaluate(() => + performance.getEntriesByType("long-animation-frame").map((frame) => ({ + duration: Math.round(frame.duration), + scripts: frame.scripts.map((s) => ({ sourceURL: s.sourceURL, invokerType: s.invokerType, duration: Math.round(s.duration) })), + })) + ); + return { result, frames, waitedMs }; + } finally { + await browser.close(); + } +} + async function summaryLine(route) { const browser = await chromium.launch(); try { @@ -286,9 +314,9 @@ describe("Third-Party-Impact-by-Domain", () => { }, 30000); it("warns when one domain holds a large share of main-thread time", async () => { - const r = await run("/heavy"); + const { result: r, frames, waitedMs } = await runAfterLongFrame("/heavy"); const other = r.items.find((i) => i.domain === "other.localhost"); - expect(other.loafMs).toBeGreaterThan(250); + expect(other.loafMs, `long animation frames after waiting ${waitedMs} ms: ${JSON.stringify(frames)}`).toBeGreaterThan(250); expect(r.issues.some((i) => i.severity === "warning" && /main-thread/.test(i.message))).toBe(true); const light = await run("/"); expect(light.issues.some((i) => /main-thread/.test(i.message))).toBe(false);