← back to NationalPaperHangers
tests/e2e-public-pages.js
100 lines
// e2e: every auth-free public route renders cleanly.
// Designed to run against prod without writing anything.
// BASE=https://nationalpaperhangers.com node tests/e2e-public-pages.js
//
// Per route, asserts:
// • HTTP 200
// • a route-specific content marker is present
// • the error template ("Something went wrong") did NOT render
// • no JS errors landed in the browser console while loading
const { chromium } = require('playwright-core');
const BASE = process.env.BASE || 'http://localhost:9765';
function findChromium() {
const paths = [
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
process.env.CHROME_PATH,
].filter(Boolean);
for (const p of paths) { try { require('fs').accessSync(p); return p; } catch {} }
return null;
}
// Each row: [path, marker text, optional skip-if-empty]
const routes = [
['/', 'National Paper Hangers'],
['/find', 'verified installer'],
['/map', 'map'],
['/watch', 'Watch'],
['/for-installers', 'installer'],
['/about', 'About'],
['/privacy', 'Privacy'],
['/terms', 'Terms'],
['/installer/atelier-bond-nyc', 'Atelier'],
['/installer/atelier-bond-nyc/book', 'consultation'],
['/login', 'Log in'],
['/signup', 'Apply to list'],
];
(async () => {
const exe = findChromium();
if (!exe) { console.error('FAIL: no Chromium'); process.exit(2); }
const browser = await chromium.launch({ executablePath: exe, headless: true });
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
const page = await ctx.newPage();
let pass = 0, fail = 0;
const assert = (c, m) => { if (c) { console.log(` ✓ ${m}`); pass++; } else { console.log(` ✗ ${m}`); fail++; } };
for (const [routePath, marker] of routes) {
const url = BASE + routePath;
console.log(`\n[${routePath}]`);
const consoleErrors = [];
const handler = msg => { if (msg.type() === 'error') consoleErrors.push(msg.text()); };
page.on('console', handler);
let resp;
try {
resp = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 12000 });
} catch (e) {
assert(false, `navigation succeeded — ${e.message}`);
page.off('console', handler);
continue;
}
const status = resp ? resp.status() : 0;
// /login and /signup share a 10/15min rate limiter; chained suite runs
// hit it. Treat 429 on those routes as a soft-pass (rate-limiter working).
if (status === 429 && /^\/(login|signup)$/.test(routePath)) {
console.log(` ⊘ rate-limited (429) — limiter is working as designed; soft-pass`);
page.off('console', handler);
pass++;
continue;
}
assert(status === 200, `HTTP 200 (got ${status})`);
const html = await page.content();
const noErrPage = !/Something went wrong|We hit a snag/i.test(html);
assert(noErrPage, 'no error template rendered');
const hasMarker = new RegExp(marker, 'i').test(html);
assert(hasMarker, `content marker "${marker}" present`);
// Filter out CSP-noise + analytics/3rd-party errors + lazy-asset 404s.
// "Failed to load resource: 404" is typically a missing image/SVG that
// an editorial template references — not a JS bug.
const ourErrors = consoleErrors.filter(e =>
!/google.*analytics|gtag|stripe|favicon|net::ERR_BLOCKED|tagmanager/i.test(e) &&
!/Failed to load resource:.*404/i.test(e)
);
assert(ourErrors.length === 0, `no first-party JS console errors (${ourErrors.length} ${ourErrors.length === 1 ? 'error' : 'errors'})`);
if (ourErrors.length) ourErrors.slice(0, 3).forEach(e => console.log(' console:', e.slice(0, 120)));
page.off('console', handler);
}
await browser.close();
console.log(`\n[result] ${pass} pass · ${fail} fail`);
process.exit(fail ? 1 : 0);
})();