diff --git a/apps/web/scripts/audit-storybook.js b/apps/web/scripts/audit-storybook.js index 0ea81746d..f5c70c1c1 100644 --- a/apps/web/scripts/audit-storybook.js +++ b/apps/web/scripts/audit-storybook.js @@ -3,6 +3,13 @@ import { chromium } from 'playwright'; import fs from 'fs'; import path from 'path'; +const BATCH_SIZE = 50; +const NAVIGATION_TIMEOUT_MS = 30000; // 30s per story navigation +const DEFAULT_TIMEOUT_MS = 300000; // 300s global default for Playwright +const MAX_RETRIES = 2; // 2 retries = 3 attempts total +const RETRY_DELAY_MS = 1500; +const POST_LOAD_WAIT_MS = 150; + console.log("Script started"); /** Ignore Storybook manager/addon requests; only count app and API failures. */ @@ -17,6 +24,78 @@ function isAppRelevantFailure(url) { } } +/** Process a single story: navigate with retries and collect errors. */ +async function processStory(page, storyId, report) { + const storyUrl = `http://localhost:6007/iframe.html?id=${storyId}&viewMode=story`; + const storyDetails = { + console: [], + pageErrors: [], + network: [], + navigation: null + }; + + const onConsole = (msg) => { + if (msg.type() === 'error' || msg.type() === 'warning') { + const location = msg.location(); + storyDetails.console.push({ + type: msg.type(), + text: msg.text(), + location: `${location.url}:${location.lineNumber}:${location.columnNumber}` + }); + } + }; + const onPageError = (err) => { + storyDetails.pageErrors.push({ message: err.message, stack: err.stack }); + }; + const onRequestFailed = (request) => { + const url = request.url(); + if (!isAppRelevantFailure(url)) return; + const failure = request.failure(); + const errorText = failure ? failure.errorText : 'Unknown network error'; + if (errorText === 'net::ERR_ABORTED' && /\/iframe\.html$|\/iframe$/.test(new URL(url).pathname || '')) return; + storyDetails.network.push({ url, method: request.method(), errorText }); + }; + + page.on('console', onConsole); + page.on('pageerror', onPageError); + page.on('requestfailed', onRequestFailed); + + let lastError = null; + for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { + try { + await page.goto(storyUrl, { waitUntil: 'domcontentloaded', timeout: NAVIGATION_TIMEOUT_MS }); + await page.waitForTimeout(POST_LOAD_WAIT_MS); + lastError = null; + break; + } catch (e) { + lastError = e; + if (attempt < MAX_RETRIES) { + await page.waitForTimeout(RETRY_DELAY_MS); + } else { + storyDetails.navigation = { message: e.message, stack: e.stack }; + } + } + } + + page.removeAllListeners('console'); + page.removeAllListeners('pageerror'); + page.removeAllListeners('requestfailed'); + + const errorCount = storyDetails.console.filter(c => c.type === 'error').length + + storyDetails.pageErrors.length + + storyDetails.network.length + + (storyDetails.navigation ? 1 : 0); + + if (errorCount > 0) { + console.log(`[FAIL] ${storyId}: ${errorCount} errors`); + report.failures[storyId] = storyDetails; + report.metadata.storiesWithErrors++; + report.metadata.totalErrors += errorCount; + } + + return errorCount; +} + async function audit() { console.log("Entering audit function"); @@ -30,8 +109,6 @@ async function audit() { process.exit(1); } - const page = await browser.newPage(); - const storybookStaticPath = path.resolve(process.cwd(), 'storybook-static/index.json'); console.log(`Reading index from ${storybookStaticPath}`); @@ -47,7 +124,7 @@ async function audit() { process.exit(1); } - console.log(`Found ${stories.length} stories.`); + console.log(`Found ${stories.length} stories. (Batch size: ${BATCH_SIZE}, nav timeout: ${NAVIGATION_TIMEOUT_MS}ms, max retries: ${MAX_RETRIES})`); const report = { metadata: { @@ -59,86 +136,36 @@ async function audit() { failures: {} }; - for (const [index, storyId] of stories.entries()) { - const storyUrl = `http://localhost:6007/iframe.html?id=${storyId}&viewMode=story`; - - // Structured error collection - const storyDetails = { - console: [], - pageErrors: [], - network: [], - navigation: null - }; - - // Capture console messages (all types, but we flag errors) - page.on('console', msg => { - if (msg.type() === 'error' || msg.type() === 'warning') { - const location = msg.location(); - storyDetails.console.push({ - type: msg.type(), - text: msg.text(), - location: `${location.url}:${location.lineNumber}:${location.columnNumber}` - }); - } - }); - - // Capture page crashes/exceptions - page.on('pageerror', err => { - storyDetails.pageErrors.push({ - message: err.message, - stack: err.stack - }); - }); - - // Capture network failures (exclude Storybook internal sb-manager/sb-addons) - // Ignore net::ERR_ABORTED on iframe/document - often navigation-induced, not app bug - page.on('requestfailed', request => { - const url = request.url(); - if (!isAppRelevantFailure(url)) return; - const failure = request.failure(); - const errorText = failure ? failure.errorText : 'Unknown network error'; - if (errorText === 'net::ERR_ABORTED' && /\/iframe\.html$|\/iframe$/.test(new URL(url).pathname || '')) return; - storyDetails.network.push({ - url, - method: request.method(), - errorText - }); - }); - - try { - await page.goto(storyUrl, { waitUntil: 'domcontentloaded', timeout: 5000 }); - // Small wait to catch immediate render errors - await page.waitForTimeout(100); - } catch (e) { - storyDetails.navigation = { - message: e.message, - stack: e.stack - }; - } - - page.removeAllListeners('console'); - page.removeAllListeners('pageerror'); - page.removeAllListeners('requestfailed'); - - // Check if there were any errors - const errorCount = storyDetails.console.filter(c => c.type === 'error').length + - storyDetails.pageErrors.length + - storyDetails.network.length + - (storyDetails.navigation ? 1 : 0); - - if (errorCount > 0) { - console.log(`[FAIL] ${storyId}: ${errorCount} errors`); - report.failures[storyId] = storyDetails; - report.metadata.storiesWithErrors++; - report.metadata.totalErrors += errorCount; - } - - if (index % 50 === 0) { - console.log(`Processed ${index}/${stories.length}...`); - } + const batches = []; + for (let i = 0; i < stories.length; i += BATCH_SIZE) { + batches.push(stories.slice(i, i + BATCH_SIZE)); } - console.log(`Audit complete.`); + for (let b = 0; b < batches.length; b++) { + const batch = batches[b]; + const batchStart = b * BATCH_SIZE; + let page; + try { + page = await browser.newPage(); + page.setDefaultTimeout(DEFAULT_TIMEOUT_MS); + } catch (e) { + console.error(`Failed to create page for batch ${b + 1}:`, e.message); + process.exit(1); + } + + for (let i = 0; i < batch.length; i++) { + const storyId = batch[i]; + const globalIndex = batchStart + i; + await processStory(page, storyId, report); + if (globalIndex % 50 === 0) { + console.log(`Processed ${globalIndex}/${stories.length}...`); + } + } + + await page.close().catch(() => {}); + } + + console.log("Audit complete."); console.log(`Stories with errors: ${report.metadata.storiesWithErrors}`); console.log(`Total errors captured: ${report.metadata.totalErrors}`);