From 1dc8f3f13718f622127acb11ecb59b4fffe84bdc Mon Sep 17 00:00:00 2001 From: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com> Date: Fri, 23 Oct 2026 08:35:31 +0200 Subject: [PATCH 1/3] fix(js-render): pass the ready-signal timeout in Playwright's options slot `page.waitForFunction(fn, { timeout: 2500 })` hands the options object to the page function as its argument, so the wait fell through to Playwright's 30 s default on every snippet that never sets `window.__anyplotReady` (1,250 of the 1,294 JavaScript implementations). The options now sit in the third slot. Measured on bar-basic/chartjs: 30.8 s before, 3.2 s after, pixel-identical PNG. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_0175hA62juZoajMcqKxWFGza --- automation/js-render/render.mjs | 6 +++++- changelog.d/js-render-ready-timeout.md | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 changelog.d/js-render-ready-timeout.md diff --git a/automation/js-render/render.mjs b/automation/js-render/render.mjs index 2ad25ab4c5b..ce0d0e0edd7 100644 --- a/automation/js-render/render.mjs +++ b/automation/js-render/render.mjs @@ -362,8 +362,12 @@ async function main() { // fall back to fonts-ready + a double rAF, which is sufficient for the // animation:false static renders the library prompts mandate. This is the // page.waitForFunction(...) signal the design calls for — never a time.sleep. + // Playwright's signature is (pageFunction, arg, options): the options object + // must sit in the third slot, or it becomes the page function's argument + // and the wait runs to the 30 s default timeout on every snippet without + // the signal. try { - await page.waitForFunction(() => window.__anyplotReady === true, { timeout: 2500 }); + await page.waitForFunction(() => window.__anyplotReady === true, null, { timeout: 2500 }); } catch { // No explicit signal — settle deterministically instead. await page.evaluate(async () => { diff --git a/changelog.d/js-render-ready-timeout.md b/changelog.d/js-render-ready-timeout.md new file mode 100644 index 00000000000..31f5def8778 --- /dev/null +++ b/changelog.d/js-render-ready-timeout.md @@ -0,0 +1,8 @@ +### Fixed + +- **JavaScript render harness waits 2.5 s, not 30 s, for the ready signal.** + `automation/js-render/render.mjs` passed the `{ timeout: 2500 }` options to + `page.waitForFunction` in the argument slot, so every snippet without + `window.__anyplotReady` sat through Playwright's 30 s default before the + fonts-ready fallback ran. The options now sit in the third slot, which + shortens every such render by about 27 s across all JavaScript libraries. From cdfa3cd06a49aff89d2f85d8c554c9f01cf0fffe Mon Sep 17 00:00:00 2001 From: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com> Date: Fri, 23 Oct 2026 08:35:54 +0200 Subject: [PATCH 2/3] docs(changelog): reference #12248 Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_0175hA62juZoajMcqKxWFGza --- changelog.d/js-render-ready-timeout.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.d/js-render-ready-timeout.md b/changelog.d/js-render-ready-timeout.md index 31f5def8778..cf156ac6ca1 100644 --- a/changelog.d/js-render-ready-timeout.md +++ b/changelog.d/js-render-ready-timeout.md @@ -6,3 +6,4 @@ `window.__anyplotReady` sat through Playwright's 30 s default before the fonts-ready fallback ran. The options now sit in the third slot, which shortens every such render by about 27 s across all JavaScript libraries. + (#12248) From 7506566cb33d6b6c8bfa5e890d2f375218b19746 Mon Sep 17 00:00:00 2001 From: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com> Date: Fri, 23 Oct 2026 08:43:48 +0200 Subject: [PATCH 3/3] perf(js-render): settle directly when the snippet cannot send the ready signal Only a snippet whose source names window.__anyplotReady, or the React entry, can ever set the signal; every other snippet idled through the 2.5 s timeout before the fonts-ready fallback ran. The harness now settles those directly. Measured on bar-basic: chartjs 1.6 s (was 3.2 s after the first commit, 30.8 s on main), echarts 3.1 s (was 31.0 s), muix 0.75 s; PNGs pixel-identical. Suggested by the Gemini Flash review. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_0175hA62juZoajMcqKxWFGza --- automation/js-render/render.mjs | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/automation/js-render/render.mjs b/automation/js-render/render.mjs index ce0d0e0edd7..213aed41d53 100644 --- a/automation/js-render/render.mjs +++ b/automation/js-render/render.mjs @@ -362,18 +362,27 @@ async function main() { // fall back to fonts-ready + a double rAF, which is sufficient for the // animation:false static renders the library prompts mandate. This is the // page.waitForFunction(...) signal the design calls for — never a time.sleep. + // The signal can only arrive from a snippet whose source names it or from + // the React entry (which sets it after fonts + three rAFs), so every other + // snippet settles directly instead of idling through the timeout. // Playwright's signature is (pageFunction, arg, options): the options object // must sit in the third slot, or it becomes the page function's argument - // and the wait runs to the 30 s default timeout on every snippet without - // the signal. - try { - await page.waitForFunction(() => window.__anyplotReady === true, null, { timeout: 2500 }); - } catch { - // No explicit signal — settle deterministically instead. - await page.evaluate(async () => { + // and the wait runs to the 30 s default timeout. + const settle = () => + page.evaluate(async () => { await (document.fonts ? document.fonts.ready : Promise.resolve()); await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r))); }); + const expectsReadySignal = bundleInfo.framework === "react" || snippetSource.includes("__anyplotReady"); + if (expectsReadySignal) { + try { + await page.waitForFunction(() => window.__anyplotReady === true, null, { timeout: 2500 }); + } catch { + // The signal never came (a guarded `finished` handler, say) — settle instead. + await settle(); + } + } else { + await settle(); } // Surface snippet failures loudly — a blank/half-drawn PNG must fail the