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. */ function isAppRelevantFailure(url) { try { const pathname = new URL(url).pathname; if (pathname.startsWith('/sb-manager/') || pathname.startsWith('/sb-addons/') || pathname.startsWith('/sb-common-assets/')) return false; if (pathname === '/index.json' || pathname === '/project.json') return false; return true; } catch { return true; } } /** 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"); let browser; try { console.log("Launching browser..."); browser = await chromium.launch({ headless: true }); console.log("Browser launched"); } catch (e) { console.error("Failed to launch browser:", e); process.exit(1); } const storybookStaticPath = path.resolve(process.cwd(), 'storybook-static/index.json'); console.log(`Reading index from ${storybookStaticPath}`); let stories = []; try { if (!fs.existsSync(storybookStaticPath)) { throw new Error(`File not found: ${storybookStaticPath}`); } const indexJson = JSON.parse(fs.readFileSync(storybookStaticPath, 'utf8')); stories = Object.values(indexJson.entries).map(e => e.id); } catch (e) { console.error(`Failed to read local index.json: ${e.message}`); process.exit(1); } console.log(`Found ${stories.length} stories. (Batch size: ${BATCH_SIZE}, nav timeout: ${NAVIGATION_TIMEOUT_MS}ms, max retries: ${MAX_RETRIES})`); const report = { metadata: { timestamp: new Date().toISOString(), totalStories: stories.length, storiesWithErrors: 0, totalErrors: 0 }, failures: {} }; const batches = []; for (let i = 0; i < stories.length; i += BATCH_SIZE) { batches.push(stories.slice(i, i + BATCH_SIZE)); } 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}`); const outputPath = path.resolve(process.cwd(), 'storybook_audit_detailed.json'); fs.writeFileSync(outputPath, JSON.stringify(report, null, 2)); console.log(`Detailed report saved to: ${outputPath}`); await browser.close(); if (report.metadata.storiesWithErrors > 0 || report.metadata.totalErrors > 0) { process.exit(1); } } audit().catch(e => { console.error("Top level error:", e); process.exit(1); });