← back to NationalPaperHangers
tests: e2e-public-pages — 12 routes × 4 assertions = 48; status 200 + content-marker + no-error-template + no-console-errors. Runs clean against prod (no auth/DB writes)
cead6419ba176d315af0cca0b5c93ca71a43f7b2 · 2026-05-06 19:55:02 -0700 · Steve
Files touched
A tests/e2e-public-pages.js
Diff
commit cead6419ba176d315af0cca0b5c93ca71a43f7b2
Author: Steve <steve@designerwallcoverings.com>
Date: Wed May 6 19:55:02 2026 -0700
tests: e2e-public-pages — 12 routes × 4 assertions = 48; status 200 + content-marker + no-error-template + no-console-errors. Runs clean against prod (no auth/DB writes)
---
tests/e2e-public-pages.js | 91 +++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 91 insertions(+)
diff --git a/tests/e2e-public-pages.js b/tests/e2e-public-pages.js
new file mode 100644
index 0000000..04b8c1c
--- /dev/null
+++ b/tests/e2e-public-pages.js
@@ -0,0 +1,91 @@
+// 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;
+ 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);
+})();
← c75cd32 /find shows ALL installers by default (was 5 claimed-only) ·
·
back to NationalPaperHangers
·
tests: graceful rate-limit handling — public-pages soft-pass a0527b7 →