[object Object]

← back to NationalPaperHangers

tests: e2e suite runner — npm run test:e2e runs all e2e-*.js sequentially, exit-78 = skip; claim-flow detects 429 from prior runs and skips cleanly. 36/0/1

698afa012831bbc8376ff5eba6e88d3fd5232bc9 · 2026-05-06 19:37:33 -0700 · Steve

Files touched

Diff

commit 698afa012831bbc8376ff5eba6e88d3fd5232bc9
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed May 6 19:37:33 2026 -0700

    tests: e2e suite runner — npm run test:e2e runs all e2e-*.js sequentially, exit-78 = skip; claim-flow detects 429 from prior runs and skips cleanly. 36/0/1
---
 package.json            |  2 ++
 tests/e2e-claim-flow.js |  9 +++++++-
 tests/run-e2e.js        | 58 +++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 68 insertions(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 1807e01..6693a84 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,8 @@
     "schema": "psql $PGDATABASE < db/schema.sql",
     "seed": "psql $PGDATABASE < db/seed.sql",
     "test": "node --test tests/smoke.test.js tests/compliance.test.js",
+    "test:e2e": "node tests/run-e2e.js",
+    "test:all": "npm test && npm run test:e2e",
     "dm-drafts": "node scripts/generate-ig-dm-drafts.js",
     "go-live-check": "node scripts/go-live-check.js",
     "gen-secrets": "node scripts/gen-secrets.js"
diff --git a/tests/e2e-claim-flow.js b/tests/e2e-claim-flow.js
index 68b63f8..51b28d2 100644
--- a/tests/e2e-claim-flow.js
+++ b/tests/e2e-claim-flow.js
@@ -82,7 +82,14 @@ function findChromium() {
   try {
     // ─── STEP 1 · GET /claim ──────────────────────────────────
     console.log(`\n[step 1] GET /installer/${TARGET_SLUG}/claim`);
-    await page.goto(`${BASE}/installer/${TARGET_SLUG}/claim`, { waitUntil: 'domcontentloaded' });
+    const r1 = await page.goto(`${BASE}/installer/${TARGET_SLUG}/claim`, { waitUntil: 'domcontentloaded' });
+    // The claim route is rate-limited (5/hr/IP). Back-to-back e2e runs trip it;
+    // exit with code 78 (skipped) rather than fail the suite.
+    if (r1 && r1.status() === 429) {
+      console.log('  ⊘ SKIP — claim rate-limit (429) tripped from prior runs; wait ~60min and retry');
+      await pg.end(); await browser.close();
+      process.exit(78);
+    }
     const emailInput = await page.$('input[type="email"], input[name="email"]');
     assert(!!emailInput, 'claim form has email input');
 
diff --git a/tests/run-e2e.js b/tests/run-e2e.js
new file mode 100644
index 0000000..d65ed4f
--- /dev/null
+++ b/tests/run-e2e.js
@@ -0,0 +1,58 @@
+#!/usr/bin/env node
+// Runs every tests/e2e-*.js sequentially. Aggregates pass/fail counts.
+// Exits non-zero if any test bailed.
+//
+// Usage:
+//   npm run test:e2e
+//   BASE=https://nationalpaperhangers.com npm run test:e2e   (against prod)
+
+const { spawnSync } = require('child_process');
+const fs = require('fs');
+const path = require('path');
+
+const TESTS_DIR = path.join(__dirname);
+const e2eFiles = fs.readdirSync(TESTS_DIR)
+  .filter(f => /^e2e-.*\.js$/.test(f))
+  .sort();
+
+if (!e2eFiles.length) {
+  console.error('No e2e-*.js files found in tests/');
+  process.exit(1);
+}
+
+console.log(`\n=== NPH e2e suite — ${e2eFiles.length} test file(s) ===\n`);
+
+let totalPass = 0, totalFail = 0, totalSkip = 0;
+const results = [];
+
+for (const f of e2eFiles) {
+  const fp = path.join(TESTS_DIR, f);
+  console.log(`▶ running ${f}`);
+  console.log('─'.repeat(60));
+  const t0 = Date.now();
+  const res = spawnSync('node', [fp], { stdio: 'pipe', encoding: 'utf8', env: process.env });
+  const dt = ((Date.now() - t0) / 1000).toFixed(1);
+  const out = (res.stdout || '') + (res.stderr || '');
+  process.stdout.write(out);
+  // Convention: exit 78 = skipped (e.g. rate-limit, missing fixture, prereq).
+  const skipped = res.status === 78;
+  const m = out.match(/\[result\]\s+(\d+)\s+pass\s+·\s+(\d+)\s+fail/);
+  const filePass = m ? parseInt(m[1], 10) : 0;
+  const fileFail = m ? parseInt(m[2], 10) : (res.status === 0 || skipped ? 0 : 1);
+  totalPass += filePass; totalFail += fileFail; if (skipped) totalSkip++;
+  results.push({ file: f, pass: filePass, fail: fileFail, skipped, dt, exit: res.status });
+  console.log('');
+}
+
+console.log('═'.repeat(60));
+console.log(`Suite summary:`);
+for (const r of results) {
+  const tag = r.skipped ? '⊘' : (r.fail ? '✗' : '✓');
+  const note = r.skipped ? ' · SKIPPED' : '';
+  console.log(`  ${tag} ${r.file.padEnd(34)}  ${r.pass} pass · ${r.fail} fail  (${r.dt}s, exit ${r.exit})${note}`);
+}
+console.log('─'.repeat(60));
+console.log(`  TOTAL                              ${totalPass} pass · ${totalFail} fail · ${totalSkip} skipped`);
+console.log('');
+
+process.exit(totalFail ? 1 : 0);

← eb62adf tests: e2e claim flow — landing form → email-domain check →  ·  back to NationalPaperHangers  ·  /find shows ALL installers by default (was 5 claimed-only) · c75cd32 →