[object Object]

← back to NationalPaperHangers

tests: e2e-business-invariants — 27 prod-runnable assertions for content rules that have regressed today (no /book CTAs on unclaimed × 6 templates, /find sort+density, /watch renders, referral disclaimer on /privacy /terms). Auto-detects local vs prod slug

d145b795e92e863112b73253c3d6600eac195cb0 · 2026-05-06 21:17:26 -0700 · Steve

Files touched

Diff

commit d145b795e92e863112b73253c3d6600eac195cb0
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed May 6 21:17:26 2026 -0700

    tests: e2e-business-invariants — 27 prod-runnable assertions for content rules that have regressed today (no /book CTAs on unclaimed × 6 templates, /find sort+density, /watch renders, referral disclaimer on /privacy /terms). Auto-detects local vs prod slug
---
 tests/e2e-business-invariants.js | 106 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 106 insertions(+)

diff --git a/tests/e2e-business-invariants.js b/tests/e2e-business-invariants.js
new file mode 100644
index 0000000..5695767
--- /dev/null
+++ b/tests/e2e-business-invariants.js
@@ -0,0 +1,106 @@
+// e2e: business invariants that have regressed at least once today.
+//
+// These check the *content* of pages, not just status code. Each catch
+// corresponds to a real bug that shipped at some point:
+//   • Schedule CTAs leaking onto unclaimed studio profiles  (fix: tick 9)
+//   • Sort + density controls missing on /find              (rule: 2026-05-06)
+//   • social-videos partial rendering self-claimed handles  (fix: 2026-05-06)
+//   • /watch page failing to render after curated_videos     (rule: 2026-05-06)
+//
+// Prod-runnable. No auth, no DB writes. Skipped on remote-base only when
+// the test inherently can't be checked over HTTP (none currently).
+//
+// Run: BASE=https://nationalpaperhangers.com node tests/e2e-business-invariants.js
+
+const { chromium } = require('playwright-core');
+const BASE = process.env.BASE || 'http://localhost:9765';
+// Default differs by environment — local seed has marquee, prod has ridgeway.
+const _isLocal = /localhost|127\.0\.0\.1/.test(BASE);
+const UNCLAIMED_SLUG = process.env.UNCLAIMED_SLUG || (_isLocal ? 'demo-unclaimed-marquee-finishes' : 'demo-unclaimed-ridgeway-paper');
+const CLAIMED_SLUG = process.env.CLAIMED_SLUG || 'atelier-bond-nyc';
+
+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;
+}
+
+(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++; } };
+
+  try {
+    // ─── Invariant 1 · UNCLAIMED studio shows NO schedule CTAs ─────────
+    // Across all 6 templates, an unclaimed studio's profile must hide the
+    // /book link entirely (per Steve's "until installer signs up, just info"
+    // directive). Visitors should see studio info + a Claim CTA.
+    console.log(`\n[invariant 1] unclaimed studio = no schedule CTAs in any of 6 templates`);
+    for (const tpl of ['editorial','trade-pro','concierge','studio','heritage','bilingue']) {
+      const url = `${BASE}/installer/${UNCLAIMED_SLUG}?_preview=${tpl}`;
+      const r = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 12000 });
+      assert(r && r.status() === 200, `${tpl} renders 200`);
+      const html = await page.content();
+      const bookLinks = (html.match(/href="\/installer\/[^"]+\/book"/g) || []).length;
+      assert(bookLinks === 0, `${tpl} has 0 /book CTAs (got ${bookLinks})`);
+      // Conversely, must show the Claim listing prompt
+      const hasClaim = /\/claim/.test(html) && /[Cc]laim/.test(html);
+      assert(hasClaim, `${tpl} shows Claim CTA`);
+    }
+
+    // ─── Invariant 2 · /find has sort + density per CLAUDE.md rule ─────
+    console.log(`\n[invariant 2] /find sort + density controls`);
+    await page.goto(`${BASE}/find`, { waitUntil: 'domcontentloaded', timeout: 12000 });
+    assert(!!(await page.$('#sort-select')), '#sort-select present');
+    assert(!!(await page.$('#density-slider')), '#density-slider present');
+    const sortOpts = await page.$$eval('#sort-select option', els => els.length);
+    assert(sortOpts >= 5, `sort has at least 5 options (got ${sortOpts})`);
+
+    // ─── Invariant 3 · /watch renders without errors ───────────────────
+    console.log(`\n[invariant 3] /watch page renders cleanly`);
+    const consoleErrors = [];
+    const handler = msg => { if (msg.type() === 'error') consoleErrors.push(msg.text()); };
+    page.on('console', handler);
+    const watchResp = await page.goto(`${BASE}/watch`, { waitUntil: 'domcontentloaded', timeout: 12000 });
+    assert(watchResp && watchResp.status() === 200, '/watch HTTP 200');
+    const watchHtml = await page.content();
+    assert(!/Something went wrong|We hit a snag/i.test(watchHtml), '/watch did not render error template');
+    page.off('console', handler);
+    const ourErrors = consoleErrors.filter(e =>
+      !/google.*analytics|gtag|stripe|favicon|tagmanager/i.test(e) &&
+      !/Failed to load resource:.*404/i.test(e)
+    );
+    assert(ourErrors.length === 0, `/watch — no first-party JS errors (${ourErrors.length})`);
+
+    // ─── Invariant 4 · /find toggles between verified-only and full directory ─────
+    console.log(`\n[invariant 4] /find verified-only default + show-all toggle`);
+    await page.goto(`${BASE}/find`, { waitUntil: 'domcontentloaded' });
+    const showToggle = await page.$('a:has-text("directory listings")').catch(() => null);
+    assert(!!showToggle, 'show/hide directory listings toggle present');
+
+    // ─── Invariant 5 · privacy + terms have correct DW-disclaimer text ────
+    console.log(`\n[invariant 5] privacy + terms include the referral-service disclaimer`);
+    for (const path of ['/privacy', '/terms']) {
+      await page.goto(`${BASE}${path}`, { waitUntil: 'domcontentloaded', timeout: 12000 });
+      const html = await page.content();
+      const hasReferralLang = /referral.*lead.coordination|directory|installer|National Paper Hangers/i.test(html);
+      assert(hasReferralLang, `${path} has referral-service / directory language`);
+    }
+
+  } catch (err) {
+    console.error('\nFATAL:', err.message);
+    fail++;
+  } finally {
+    await browser.close();
+    console.log(`\n[result] ${pass} pass · ${fail} fail`);
+    process.exit(fail ? 1 : 0);
+  }
+})();

← 2ff46f6 ops: launchd-driven e2e prod-monitor (every 6h @ :17), local  ·  back to NationalPaperHangers  ·  og: add og:image + twitter:image to every page (default bran 442cc6c →