import { test, expect, Page, Request, Response } from '@playwright/test'; import * as fs from 'fs'; import * as path from 'path'; interface RouteIssue { route: string; consoleErrors: string[]; consoleWarnings: string[]; networkFailures: Array<{ url: string; status?: number; failure?: string; }>; } /** * Recursively scans the Next.js app directory to discover all routes. * Dynamic segments like [id] are replaced with "demo". */ function discoverRoutes(appDir: string): string[] { const routes: Set = new Set(); function scanDirectory(dir: string, routePrefix: string = ''): void { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { // Handle dynamic segments: [id] -> demo const segment = entry.name.match(/^\[(.+)\]$/) ? 'demo' : entry.name; // Skip special Next.js directories if (!entry.name.startsWith('_') && !entry.name.startsWith('.')) { const newPrefix = routePrefix + '/' + segment; scanDirectory(fullPath, newPrefix); } } else if (entry.isFile() && entry.name === 'page.tsx') { // Found a page component - this defines a route const route = routePrefix === '' ? '/' : routePrefix; routes.add(route); } } } scanDirectory(appDir); // Return sorted, deduplicated list return Array.from(routes).sort(); } /** * Attaches listeners to capture console errors/warnings and network failures * for a specific route visit. */ function setupIssueCapture(page: Page, issues: RouteIssue): void { // Capture console errors and warnings page.on('console', (msg) => { const type = msg.type(); if (type === 'error') { issues.consoleErrors.push(msg.text()); } else if (type === 'warning') { issues.consoleWarnings.push(msg.text()); } }); // Capture request failures page.on('requestfailed', (request: Request) => { issues.networkFailures.push({ url: request.url(), failure: request.failure()?.errorText || 'Request failed', }); }); // Capture non-2xx/3xx responses page.on('response', (response: Response) => { const status = response.status(); // Consider 4xx and 5xx as failures if (status >= 400) { issues.networkFailures.push({ url: response.url(), status, }); } }); } /** * Formats aggregated issues into a readable failure report. */ function formatFailureReport(failedRoutes: RouteIssue[]): string { const lines: string[] = [ '', '========================================', 'SMOKE TEST FAILURES', '========================================', '', ]; for (const issue of failedRoutes) { lines.push(`Route: ${issue.route}`); lines.push('----------------------------------------'); if (issue.consoleErrors.length > 0) { lines.push('Console Errors:'); issue.consoleErrors.forEach((err) => { lines.push(` - ${err}`); }); lines.push(''); } if (issue.consoleWarnings.length > 0) { lines.push('Console Warnings:'); issue.consoleWarnings.forEach((warn) => { lines.push(` - ${warn}`); }); lines.push(''); } if (issue.networkFailures.length > 0) { lines.push('Network Failures:'); issue.networkFailures.forEach((fail) => { const statusPart = fail.status ? ` [${fail.status}]` : ''; const failurePart = fail.failure ? ` (${fail.failure})` : ''; lines.push(` - ${fail.url}${statusPart}${failurePart}`); }); lines.push(''); } lines.push(''); } lines.push('========================================'); return lines.join('\n'); } test.describe('Website Smoke Test', () => { test.describe.configure({ mode: 'serial' }); let allRoutes: string[]; test.beforeAll(() => { // Discover all routes from the app directory const appDir = path.resolve(process.cwd(), 'apps/website/app'); allRoutes = discoverRoutes(appDir); console.log(`Discovered ${allRoutes.length} routes:`); allRoutes.forEach((route) => console.log(` ${route}`)); }); test('all pages load without console errors or network failures', async ({ page }) => { const failedRoutes: RouteIssue[] = []; for (const route of allRoutes) { const issues: RouteIssue = { route, consoleErrors: [], consoleWarnings: [], networkFailures: [], }; // Setup listeners before navigation setupIssueCapture(page, issues); try { // Navigate to the route and wait for network to settle await page.goto(route, { waitUntil: 'networkidle', timeout: 30000, }); // Small delay to catch any late console messages await page.waitForTimeout(500); } catch (error) { // Navigation failure itself issues.networkFailures.push({ url: route, failure: `Navigation error: ${error instanceof Error ? error.message : String(error)}`, }); } // Remove listeners for next iteration page.removeAllListeners('console'); page.removeAllListeners('requestfailed'); page.removeAllListeners('response'); // Check if this route had any issues const hasIssues = issues.consoleErrors.length > 0 || issues.consoleWarnings.length > 0 || issues.networkFailures.length > 0; if (hasIssues) { failedRoutes.push(issues); } } // Report all failures at once if (failedRoutes.length > 0) { const report = formatFailureReport(failedRoutes); expect(failedRoutes, report).toHaveLength(0); } }); });