From 5c068ace81cacae1b85f69826cd7aad4a616e1c3 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Sun, 1 Nov 2026 17:02:38 +0800 Subject: [PATCH] fix(ci): complete deferred script loading before brand assertions --- .../waterfall-wiki/tests/brand-navigation.mjs | 2 +- .../tests/brand-navigation.test.mjs | 16 +++++++----- .../tests/brand-ui-integration.ts | 26 +++++++++++++++---- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/apps/waterfall-wiki/tests/brand-navigation.mjs b/apps/waterfall-wiki/tests/brand-navigation.mjs index 60ac8f5..21a022d 100644 --- a/apps/waterfall-wiki/tests/brand-navigation.mjs +++ b/apps/waterfall-wiki/tests/brand-navigation.mjs @@ -21,7 +21,7 @@ export async function navigateBrandDocument(navigate, url, now = Date.now) { } throw new Error(`Document navigation failed: ${url}, HTTP ${status ?? 'no response'}`); } catch (error) { - if (attempt === 1 && /ECONNRESET|net::ERR_(?:CONNECTION_RESET|CONNECTION_CLOSED|EMPTY_RESPONSE)/.test(String(error))) { + if (attempt === 1 && /ECONNRESET|net::ERR_(?:CONNECTION_RESET|CONNECTION_CLOSED|EMPTY_RESPONSE|ABORTED)/.test(String(error))) { console.warn('Brand navigation connection recovery', JSON.stringify({url, error: String(error), attempt})); continue; } diff --git a/apps/waterfall-wiki/tests/brand-navigation.test.mjs b/apps/waterfall-wiki/tests/brand-navigation.test.mjs index a16eee7..3a04f74 100644 --- a/apps/waterfall-wiki/tests/brand-navigation.test.mjs +++ b/apps/waterfall-wiki/tests/brand-navigation.test.mjs @@ -16,13 +16,15 @@ test('one proxy failure can recover within the original deadline', async () => { }, '/canvas', () => elapsed); assert.deepEqual(timeouts, [60000, 45000]); }); -test('connection reset can recover once', async () => { - let calls = 0; - await navigateBrandDocument(async () => { - if (++calls === 1) throw new Error('net::ERR_CONNECTION_RESET'); - return response(200); - }, '/canvas'); - assert.equal(calls, 2); +test('connection reset and interrupted bootstrap scripts can recover once', async () => { + for(const failure of ['net::ERR_CONNECTION_RESET','Document bootstrap script request failed: net::ERR_ABORTED']){ + let calls = 0; + await navigateBrandDocument(async () => { + if (++calls === 1) throw new Error(failure); + return response(200); + }, '/canvas'); + assert.equal(calls, 2); + } }); test('persistent server failure is not hidden', async () => { let calls = 0; diff --git a/apps/waterfall-wiki/tests/brand-ui-integration.ts b/apps/waterfall-wiki/tests/brand-ui-integration.ts index 6f1b7f1..dd0c87f 100644 --- a/apps/waterfall-wiki/tests/brand-ui-integration.ts +++ b/apps/waterfall-wiki/tests/brand-ui-integration.ts @@ -1,6 +1,6 @@ /** CI-only checks against the disposable stack, never against personal documents. */ import assert from 'node:assert/strict'; -import { chromium } from 'playwright-core'; +import { chromium, type Request } from 'playwright-core'; import { navigateBrandDocument } from './brand-navigation.mjs'; export async function verifyBrandUI(base: string) { for (const prefix of process.env.VERIFY_CANVAS_UI === '1' ? ['', '/studio'] : ['']) { @@ -19,18 +19,34 @@ export async function verifyBrandUI(base: string) { try { const page = await browser.newPage({ viewport: { width: 1440, height: 960 } }); page.setDefaultTimeout(60000);page.setDefaultNavigationTimeout(60000); - const failures: string[] = []; + const failures: string[] = [], scripts: {path:string;status?:number;ms:number;error?:string}[] = []; + const scriptStarted = new WeakMap(); + page.on('request', request => { if(request.resourceType()==='script') scriptStarted.set(request,Date.now()); }); + page.on('response', response => { if(response.request().resourceType()==='script') { scripts.push({path:new URL(response.url()).pathname,status:response.status(),ms:Date.now()-(scriptStarted.get(response.request())??Date.now())}); if(scripts.length>24)scripts.shift(); } }); page.on('pageerror', error => failures.push(error.message)); - page.on('requestfailed', request => failures.push(new URL(request.url()).pathname + ': ' + request.failure()?.errorText)); + page.on('requestfailed', request => { failures.push(new URL(request.url()).pathname + ': ' + request.failure()?.errorText); if(request.resourceType()==='script') scripts.push({path:new URL(request.url()).pathname,ms:Date.now()-(scriptStarted.get(request)??Date.now()),error:request.failure()?.errorText}); }); const visit = async (path: string, theme: string) => { const started = Date.now(); try { - await navigateBrandDocument(timeout => page.goto(base + path, { waitUntil: 'commit', timeout }), base + path); + // DOMContentLoaded also waits for this document's deferred scripts, unlike commit. + await navigateBrandDocument(async timeout => { + const attemptStarted=Date.now(), bootstrapFailures:string[]=[]; + const failed=(request:Request)=>{ + if(request.resourceType()==='script'&&(scriptStarted.get(request)??0)>=attemptStarted) + bootstrapFailures.push(request.failure()?.errorText||'unknown script failure'); + }; + page.on('requestfailed',failed); + try { + const response=await page.goto(base+path,{waitUntil:'domcontentloaded',timeout}); + if(response?.ok()&&bootstrapFailures.length)throw new Error('Document bootstrap script request failed: '+bootstrapFailures.join('; ')); + return response; + } finally {page.off('requestfailed',failed);} + }, base + path); await page.locator('button[data-theme-ready="true"]').first().waitFor(); await page.waitForFunction(expected => document.documentElement.dataset.theme === expected, theme); console.log('Brand UI route ready:', path, theme, Date.now() - started); } catch (error) { - console.error('Brand UI navigation diagnostics', JSON.stringify({path, url:page.url(), elapsedMs:Date.now()-started, failures:failures.slice(-20), body:(await page.locator('body').innerText({timeout:3000}).catch(()=>'unavailable')).slice(0,1200)})); + console.error('Brand UI navigation diagnostics', JSON.stringify({path, url:page.url(), elapsedMs:Date.now()-started, failures:failures.slice(-20), scripts:scripts.slice(-24), controls:await page.locator('[data-theme-ready]').evaluateAll(elements=>elements.map(el=>({ready:el.getAttribute('data-theme-ready'),display:getComputedStyle(el).display}))), body:(await page.locator('body').innerText({timeout:3000}).catch(()=>'unavailable')).slice(0,1200)})); throw error; } };