/** * Recover once from an idempotent document GET's transient proxy failure. * This never retries hydration, theme assertions, or mutations, and shares one * 60-second budget across both attempts rather than extending the deadline. * @param {(timeout: number) => Promise<{ok(): boolean, status(): number} | null>} navigate * @param {string} url * @param {() => number} now */ export async function navigateBrandDocument(navigate, url, now = Date.now) { const deadline = now() + 60000; for (let attempt = 1; attempt <= 2; attempt++) { const remaining = deadline - now(); if (remaining <= 0) throw new Error(`Document navigation budget exhausted: ${url}`); try { const response = await navigate(remaining); if (response?.ok()) return response; const status = response?.status(); if (attempt === 1 && [500, 502, 503, 504].includes(status ?? 0)) { console.warn('Brand navigation proxy recovery', JSON.stringify({url, status, attempt})); continue; } 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|ABORTED)/.test(String(error))) { console.warn('Brand navigation connection recovery', JSON.stringify({url, error: String(error), attempt})); continue; } throw error; } } throw new Error(`Document navigation failed: ${url}`); } /** Load a document fully before relying on server-rendered text or leaving it. * Only document GETs may recover; application assertions remain outside retry. * @param {import('playwright-core').Page} page * @param {string} url * @param {boolean} reload */ export async function visitBrowserDocument(page, url, reload = false) { return navigateBrandDocument(async timeout => { const started = Date.now(); const scripts = new Set(), failures = []; const requested = request => { if (request.resourceType() === 'script') scripts.add(request); }; const failed = request => { if (scripts.has(request)) failures.push(request.failure()?.errorText || 'unknown script failure'); }; page.on('request', requested); page.on('requestfailed', failed); try { const response = await (reload ? page.reload({waitUntil: 'domcontentloaded', timeout}) : page.goto(url, {waitUntil: 'domcontentloaded', timeout})); if (response?.ok()) { const remaining = timeout - (Date.now() - started); if (remaining <= 0) throw new Error('Document hydration budget exhausted: ' + url); try { // Responsive layouts may hide the toggle. Its mounted ready marker // proves hydration; each caller still checks the controls it needs. // A cancelled script may be a preload duplicate. Hydration is the // authority; recover only when cancellation actually blocks startup. await page.locator('button[data-theme-ready="true"]').first().waitFor({state: 'attached', timeout: failures.length ? Math.min(5000, remaining) : remaining}); } catch (error) { if (failures.length) throw new Error('Document bootstrap script request failed: ' + failures.join('; '), {cause: error}); throw error; } } return response; } finally { page.off('request', requested); page.off('requestfailed', failed); } }, url); }