/** * Browser print regression tests for Email Library print and PDF export. * * Requirements: * - Node.js (v20+) * - Playwright (`npm ci` or `npm install -D @playwright/test`) * - Chromium browser binary: * - Standard Playwright install: `npx playwright install chromium` * - Or custom executable via env vars: * - `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/path/to/chrome` * - `CHROME_PATH=/path/to/chrome` * - Or custom browsers cache dir via: * - `PLAYWRIGHT_BROWSERS_PATH=/path/to/cache` */ const http = require('http'); const fs = require('fs'); const path = require('path'); const os = require('os'); let chromium; try { ({ chromium } = require('playwright')); } catch (err) { if (err && err.code === 'MODULE_NOT_FOUND') { console.error( 'Error: Playwright module not found.\n' + 'Please install dependencies by running:\n' + ' npm ci\n' + 'or ensure NODE_PATH contains the playwright package.' ); process.exit(1); } throw err; } const ROOT = path.resolve(__dirname, '..'); // Discover available Chromium executable using standard Playwright resolution, // custom environment variables, standard cache paths, or system binaries. function findChromiumPath() { // 1. Explicit environment variable overrides if (process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH && fs.existsSync(process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH)) { return process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH; } if (process.env.CHROME_PATH && fs.existsSync(process.env.CHROME_PATH)) { return process.env.CHROME_PATH; } // 2. Playwright's configured/default executable path try { const defaultPath = chromium.executablePath(); if (defaultPath && fs.existsSync(defaultPath)) { return defaultPath; } } catch (e) { // chromium.executablePath() may throw if no default browser is installed } // 3. Inspect standard Playwright browser cache directories const cacheDirs = []; if (process.env.PLAYWRIGHT_BROWSERS_PATH) { cacheDirs.push(process.env.PLAYWRIGHT_BROWSERS_PATH); } if (process.platform === 'win32') { if (process.env.LOCALAPPDATA) { cacheDirs.push(path.join(process.env.LOCALAPPDATA, 'ms-playwright')); } cacheDirs.push(path.join(os.homedir(), 'AppData', 'Local', 'ms-playwright')); } else if (process.platform === 'darwin') { cacheDirs.push(path.join(os.homedir(), 'Library', 'Caches', 'ms-playwright')); } else { cacheDirs.push(path.join(os.homedir(), '.cache', 'ms-playwright')); } for (const cacheDir of cacheDirs) { if (!fs.existsSync(cacheDir)) continue; try { const entries = fs.readdirSync(cacheDir).sort().reverse(); for (const entry of entries) { const entryPath = path.join(cacheDir, entry); const candidates = [ path.join(entryPath, 'chrome-headless-shell-linux64', 'chrome-headless-shell'), path.join(entryPath, 'chrome-linux64', 'chrome'), path.join(entryPath, 'chrome-linux', 'chrome'), path.join(entryPath, 'chrome-win64', 'chrome.exe'), path.join(entryPath, 'chrome-win', 'chrome.exe'), path.join(entryPath, 'chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'), ]; for (const candidate of candidates) { if (fs.existsSync(candidate)) { return candidate; } } } } catch (e) {} } // 4. System browser fallbacks const systemCandidates = process.platform === 'win32' ? [ path.join(process.env['PROGRAMFILES'] || 'C:\\Program Files', 'Google', 'Chrome', 'Application', 'chrome.exe'), path.join(process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)', 'Google', 'Chrome', 'Application', 'chrome.exe'), ] : [ '/usr/bin/google-chrome', '/usr/bin/chromium', '/usr/bin/chromium-browser', '/snap/bin/chromium', ]; for (const sc of systemCandidates) { if (fs.existsSync(sc)) return sc; } return undefined; } async function run() { const server = http.createServer((req, res) => { const url = new URL(req.url, 'http://127.0.0.1'); if (url.pathname.startsWith('/api/email/read/')) { const uid = url.pathname.replace('/api/email/read/', ''); if (uid === 'fail_uid' || uid === 'net_fail') { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Mailbox disconnected' })); return; } if (uid === 'crash_network') { req.destroy(); return; } const markSeen = url.searchParams.get('mark_seen'); if (markSeen !== 'false') { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'mark_seen must be false' })); return; } let body_html = ''; if (uid === 'img_test') { body_html = [ '
Email with diverse images
', '', '
',
'',
'',
'',
'',
'${'A'.repeat(5000)}
Standard message body for ${uid}