ci(storybook): implement batch processing and retry logic for audit script
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
47e535fb46
commit
746610fbb6
1 changed files with 108 additions and 81 deletions
|
|
@ -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}`);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue