fix(ci): complete deferred script loading before brand assertions
creative workspace review checks / verify-deployed-canvas (pull_request) Successful in 55s
creative workspace review checks / verify (pull_request) Successful in 26m46s

This commit is contained in:
2026-11-01 17:02:38 +08:00
parent f4397db869
commit 5c068ace81
3 changed files with 31 additions and 13 deletions
@@ -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;
}
@@ -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;
@@ -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<Request,number>();
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;
}
};