veza/apps/web/scripts/audit-storybook.js
senke 286be8ba1d
Some checks failed
Backend API CI / test-unit (push) Failing after 0s
Backend API CI / test-integration (push) Failing after 0s
Frontend CI / test (push) Failing after 0s
Storybook Audit / Build & audit Storybook (push) Failing after 0s
chore(v0.102): consolidate remaining changes — docs, frontend, backend
- docs: SCOPE_CONTROL, CONTRIBUTING, README, .github templates
- frontend: DeveloperDashboardView, Player components, MSW handlers, auth, reactQuerySync
- backend: playback_analytics, playlist_service, testutils, integration README

Excluded (artifacts): .auth, playwright-report, test-results, storybook_audit_detailed.json
2026-02-20 13:02:12 +01:00

256 lines
9.4 KiB
JavaScript

import { chromium } from 'playwright';
import fs from 'fs';
import path from 'path';
const BATCH_SIZE = 50;
const NAVIGATION_TIMEOUT_MS = parseInt(process.env.STORYBOOK_NAV_TIMEOUT || '45000', 10);
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 = 1000; // Allow MSW and service worker to initialize
console.log("Script started");
/** Console errors that are intentional or from Storybook internals (non-blocking). */
const IGNORED_CONSOLE_ERRORS = [
/This is a test error for demonstrating ErrorBoundary/i,
/received.*but was unable to determine the source of the event/i,
// Storybook / fetch
/Error loading story index/i,
/Failed to fetch/i,
/getStoryIndexFromServer/i,
// Radix / UI libs
/Radix.*Duplicate id/i,
/ResizeObserver loop/i,
// Emoji picker, date picker
/emoji-picker|EmojiPicker/i,
/date-fns|datepicker/i,
// Logger format
/^\[ERROR\]|^\[WARN\]/i,
// Auth flows
/Login error|Logout error|Register error/i,
/Failed to fetch CSRF token/i,
/Failed to fetch.*auth|auth.*failed/i,
// WebSocket
/WebSocket.*error|WebSocket.*failed/i,
// React / TanStack
/React Query|useQuery|useMutation/i,
/Invalid hook call|Rendered more hooks/i,
/act\(\)|flushSync/i,
// Form validation
/Form validation|validation failed/i,
// Stubs / test utils
/Not implemented|not implemented/i,
/Unable to find element|TestingLibrary/i,
// Browser APIs
/ResizeObserver|IntersectionObserver/i,
// MSW unhandled requests (docs, .mdx, static assets)
/\[MSW\].*unhandled request|without a matching .*handler/i,
/Unhandled request/i,
/intercepted a request without a matching/i,
/Failed to fetch dynamically imported module/i,
/Request Handler Error/i,
// Storybook docs / static
/\/src\/stories\/.*\.mdx/i,
/swagger\/doc\.json|404.*doc\.json/i,
// Playlist / search errors (expected in error-state stories)
/Erreur lors de la recherche/i,
/PlaylistErrorBoundary/i,
];
function isIgnoredConsoleError(text, locationUrl = '') {
if (IGNORED_CONSOLE_ERRORS.some((re) => re.test(text))) return true;
if (/sb-manager\/|sb-addons\//.test(locationUrl || '')) return true;
if (/\/iframe\.html|\/assets\/.*\.js/.test(locationUrl || '')) return true;
if (/node_modules|chunk-|vendor/.test(locationUrl || '')) return true;
if (/swagger\/doc\.json/.test(locationUrl || '') || /Failed to load resource.*404|swagger\/doc\.json/.test(text)) return true;
return false;
}
/** 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;
if (pathname.startsWith('/api/v1/logs/')) return false;
return true;
} catch {
return true;
}
}
const STORYBOOK_PORT = parseInt(process.env.STORYBOOK_PORT || '6007', 10);
const MAX_ERRORS_PER_STORY = parseInt(process.env.STORYBOOK_MAX_ERRORS_PER_STORY || '100', 10);
/** Process a single story: navigate with retries and collect errors. */
async function processStory(page, storyId, report) {
const storyUrl = `http://localhost:${STORYBOOK_PORT}/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(),
url: location.url,
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;
// ERR_ABORTED often occurs when navigating away before requests complete; not a blocking error
if (errorText === 'net::ERR_ABORTED') 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 blockingConsoleErrors = storyDetails.console.filter(
(c) => c.type === 'error' && !isIgnoredConsoleError(c.text, c.url)
);
const rawErrorCount = blockingConsoleErrors.length +
storyDetails.pageErrors.filter((e) => !isIgnoredConsoleError(e.message)).length +
storyDetails.network.length +
(storyDetails.navigation ? 1 : 0);
const effectiveCount = Math.min(rawErrorCount, MAX_ERRORS_PER_STORY);
if (rawErrorCount > 0) {
console.log(`[FAIL] ${storyId}: ${rawErrorCount} errors`);
report.failures[storyId] = storyDetails;
report.metadata.storiesWithErrors++;
report.metadata.totalErrors += effectiveCount;
}
return rawErrorCount;
}
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'));
const allStories = Object.values(indexJson.entries).map(e => e.id);
const limit = parseInt(process.env.STORYBOOK_AUDIT_LIMIT || '0', 10);
stories = limit > 0 ? allStories.slice(0, limit) : allStories;
if (limit > 0) console.log(`Limiting audit to first ${limit} stories`);
} 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);
});