import { Page } from '@playwright/test'; // Console noise that is expected on a clean boot and must not fail the smoke // test. Keep this list short and specific — every entry should name a known, // understood source. const BENIGN_CONSOLE_PATTERNS: RegExp[] = [ // README: after login you may see a 404 for a missing avatar thumbnail — // "not a login failure". Also covers the generic resource-404 console line. /_matrix\/(client|media)\/v\d+\/(media\/)?thumbnail/i, /Failed to load resource: the server responded with a status of 404/i, // The login page probes `POST /_matrix/client/v3/register` to learn whether // registration is open; the homeserver answers 401 + UIA flows by design. /Failed to load resource: the server responded with a status of 401/i, // Homeserver discovery pings can fail on a runner with no outbound network. /\/\.well-known\/matrix\/client/i, /Failed to fetch|NetworkError|ERR_NAME_NOT_RESOLVED|ERR_INTERNET_DISCONNECTED/i, // Tier 3 (local homeserver named "localhost"): the client's well-known // autodiscovery probes https://localhost/, which is either not listening // (CI) or a self-signed dev server (local calls stack). /ERR_CONNECTION_REFUSED|ERR_CERT_AUTHORITY_INVALID|ERR_SSL_PROTOCOL_ERROR/i, // React devtools hint in production bundles. /Download the React DevTools/i, ]; export type ConsoleCollector = { errors: string[]; pageErrors: string[]; /** Errors not matched by the benign allowlist. */ unexpected: () => string[]; }; /** * Records console.error lines and uncaught page errors for the given page. * Attach BEFORE navigating so nothing emitted during boot is missed. */ export function collectConsole(page: Page): ConsoleCollector { const errors: string[] = []; const pageErrors: string[] = []; page.on('console', (msg) => { if (msg.type() === 'error') errors.push(msg.text()); }); page.on('pageerror', (err) => { pageErrors.push(err.message); }); return { errors, pageErrors, unexpected: () => [ ...pageErrors.map((m) => `pageerror: ${m}`), ...errors.filter((m) => !BENIGN_CONSOLE_PATTERNS.some((re) => re.test(m))), ], }; } /** * Generates a small JPEG in the browser (canvas.toBlob) and returns its bytes. * JPEG rather than PNG so the composer's "Compress image" path actually * re-encodes (compressImage() deliberately skips PNG to preserve alpha). */ export async function generateJpeg(page: Page, size = 96): Promise { const dataUrl = await page.evaluate((px) => { const canvas = document.createElement('canvas'); canvas.width = px; canvas.height = px; const ctx = canvas.getContext('2d'); if (!ctx) throw new Error('canvas 2d context unavailable'); const grad = ctx.createLinearGradient(0, 0, px, px); grad.addColorStop(0, '#7c3aed'); grad.addColorStop(1, '#f59e0b'); ctx.fillStyle = grad; ctx.fillRect(0, 0, px, px); ctx.fillStyle = '#fff'; ctx.font = `${Math.floor(px / 4)}px sans-serif`; ctx.fillText('e2e', px / 8, px / 2); return canvas.toDataURL('image/jpeg', 0.95); }, size); return Buffer.from(dataUrl.split(',')[1], 'base64'); }