[object Object]

← back to NationalPaperHangers

tests: e2e buyer wizard — 5 steps, conditional brand reveal, back-nav persistence, pill state. 16/16 pass

7c77afb76600bdf75839c4ab90a4b13e39dc4684 · 2026-05-06 17:37:36 -0700 · Steve

Files touched

Diff

commit 7c77afb76600bdf75839c4ab90a4b13e39dc4684
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed May 6 17:37:36 2026 -0700

    tests: e2e buyer wizard — 5 steps, conditional brand reveal, back-nav persistence, pill state. 16/16 pass
---
 tests/e2e-buyer-wizard.js | 156 ++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 156 insertions(+)

diff --git a/tests/e2e-buyer-wizard.js b/tests/e2e-buyer-wizard.js
new file mode 100644
index 0000000..d342b07
--- /dev/null
+++ b/tests/e2e-buyer-wizard.js
@@ -0,0 +1,156 @@
+// e2e click-test: buyer booking wizard on /installer/:slug/book.
+//
+// Walks through the 5-step intake wizard end-to-end:
+//   1. Scope    (rooms / sq ft / ceiling / surface state / access)
+//   2. Wallpaper (Yes/No → reveals brand/SKU on Yes)
+//   3. Who      (homeowner / designer / architect / contractor / property_mgr / other)
+//   4. Where    (address)
+//   5. Slot & details — fills name/email/phone but does NOT submit
+//      (avoids creating a real booking row in the DB)
+//
+// Verifies: each step is visible only when active, Back+Next nav works,
+// step-pill state advances, conditional brand reveal in step 2 works.
+//
+// Run: node tests/e2e-buyer-wizard.js
+// Env: BASE=http://localhost:9765 (default), SLUG=<paid-tier studio slug>
+
+const { chromium } = require('playwright-core');
+
+const BASE = process.env.BASE || 'http://localhost:9765';
+const SLUG = process.env.SLUG || 'rivera-installs-miami';  // Pro tier in seed
+
+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 binary'); process.exit(2); }
+  console.log(`[e2e-buyer] using browser: ${exe}`);
+
+  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 = (cond, msg) => { if (cond) { console.log(`  ✓ ${msg}`); pass++; } else { console.log(`  ✗ ${msg}`); fail++; } };
+
+  try {
+    // ─── Setup · /book reachable + paid-tier studio with calendar enabled ───
+    console.log(`\n[setup] GET /installer/${SLUG}/book`);
+    await page.goto(`${BASE}/installer/${SLUG}/book`, { waitUntil: 'domcontentloaded' });
+    const title = await page.title();
+    assert(/Book/i.test(title), `page title contains "Book" (got "${title}")`);
+
+    // Verify wizard rendered (calendar enabled)
+    const wizard = await page.$('[data-intake-wizard]');
+    assert(!!wizard, 'wizard form rendered (calendar enabled for this studio)');
+    if (!wizard) {
+      console.log('\n  Studio has no calendar — pick a paid-tier slug via SLUG=...');
+      await browser.close();
+      process.exit(2);
+    }
+
+    const stepCount = await page.$$eval('.intake-step', els => els.length);
+    assert(stepCount === 5, `5 step fieldsets present (got ${stepCount})`);
+    const pillCount = await page.$$eval('[data-step-pill]', els => els.length);
+    assert(pillCount === 5, `5 step pills present (got ${pillCount})`);
+
+    // ─── Step 1 · Scope ────────────────────────────────────────────
+    console.log('\n[step 1] Scope');
+    const step1Active = await page.$eval('[data-step="1"]', el => el.classList.contains('is-active'));
+    assert(step1Active, 'step 1 is active on load');
+
+    await page.fill('input[name="surfaces"]', 'Dining room walls + ceiling');
+    await page.fill('input[name="square_feet"]', '320');
+    await page.fill('input[name="ceiling_height_ft"]', '10');
+    await page.selectOption('select[name="surface_state"]', 'painted_drywall');
+    await page.selectOption('select[name="access_constraints"]', 'ground_floor');
+    await page.click('[data-step-next="2"]');
+    await page.waitForFunction(() => document.querySelector('[data-step="2"]').classList.contains('is-active'));
+    assert(true, 'advanced to step 2');
+
+    // ─── Step 2 · Wallpaper ────────────────────────────────────────
+    console.log('\n[step 2] Wallpaper');
+    // "Yes — already chosen" reveals brand fields
+    await page.locator('label').filter({ has: page.locator('input[name="product_sourced"][value="true"]') }).click();
+    await page.waitForFunction(() => {
+      const el = document.querySelector('[data-show-if]');
+      return el && getComputedStyle(el).display !== 'none';
+    }, { timeout: 3000 }).catch(() => {});
+    const brandFieldsVisible = await page.$eval('[data-show-if]', el => getComputedStyle(el).display !== 'none');
+    assert(brandFieldsVisible, 'brand fields revealed when product_sourced=Yes');
+
+    await page.selectOption('select[name="material"]', 'silk');
+    await page.fill('input[name="brand"]', 'de Gournay');
+    await page.fill('input[name="brand_sku"]', 'Earlham 7821');
+    await page.click('[data-step-next="3"]');
+    await page.waitForFunction(() => document.querySelector('[data-step="3"]').classList.contains('is-active'));
+    assert(true, 'advanced to step 3');
+
+    // ─── Step 3 · Who ─────────────────────────────────────────────
+    console.log('\n[step 3] Who is ordering');
+    await page.locator('label').filter({ has: page.locator('input[name="customer_role"][value="designer"]') }).click();
+    const roleChecked = await page.$eval('input[name="customer_role"][value="designer"]', el => el.checked);
+    assert(roleChecked, '"designer" role selected');
+    await page.click('[data-step-next="4"]');
+    await page.waitForFunction(() => document.querySelector('[data-step="4"]').classList.contains('is-active'));
+    assert(true, 'advanced to step 4');
+
+    // ─── Step 4 · Where ───────────────────────────────────────────
+    console.log('\n[step 4] Where');
+    await page.fill('input[name="address_line1"]', '1234 Sunset Blvd');
+    await page.fill('input[name="city"]', 'Los Angeles');
+    await page.fill('input[name="state"]', 'CA');
+    await page.fill('input[name="zip"]', '90028');
+    await page.click('[data-step-next="5"]');
+    await page.waitForFunction(() => document.querySelector('[data-step="5"]').classList.contains('is-active'));
+    assert(true, 'advanced to step 5');
+
+    // ─── Step 5 · Slot & details ─────────────────────────────────
+    console.log('\n[step 5] Slot & details (filling but NOT submitting)');
+    await page.fill('input[name="customer_name"]', 'Test Buyer (e2e)');
+    await page.fill('input[name="customer_email"]', 'e2e+buyer@example.com');
+    await page.fill('input[name="customer_phone"]', '+1 555 555 0100');
+    const nameVal = await page.$eval('input[name="customer_name"]', el => el.value);
+    assert(nameVal === 'Test Buyer (e2e)', 'name field accepts input');
+
+    // ─── Step pills reflect progression ──────────────────────────
+    console.log('\n[step-pill state]');
+    const pillState = await page.$$eval('[data-step-pill]', els => els.map(e => ({
+      n: e.getAttribute('data-step-pill'),
+      done: e.classList.contains('is-done'),
+      active: e.classList.contains('is-active')
+    })));
+    assert(pillState[4].active, 'pill 5 marked active');
+    assert(pillState[0].done && pillState[1].done && pillState[2].done && pillState[3].done, 'pills 1-4 marked done');
+
+    // ─── Back navigation works ───────────────────────────────────
+    console.log('\n[nav] Back from step 5 → step 4');
+    await page.click('[data-step-prev="4"]');
+    await page.waitForFunction(() => document.querySelector('[data-step="4"]').classList.contains('is-active'));
+    const addrVal = await page.$eval('input[name="address_line1"]', el => el.value);
+    assert(addrVal === '1234 Sunset Blvd', 'address persists when nav-ing back');
+
+    // ─── Step 2 conditional reveal toggles correctly ─────────────
+    console.log('\n[regression] product_sourced toggle');
+    await page.click('[data-step-prev="3"]');  // back to step 3
+    await page.click('[data-step-prev="2"]');  // back to step 2
+    await page.locator('label').filter({ has: page.locator('input[name="product_sourced"][value="false"]') }).click();
+    const brandFieldsHidden = await page.$eval('[data-show-if]', el => getComputedStyle(el).display === 'none');
+    assert(brandFieldsHidden, 'brand fields hide when product_sourced=No');
+
+  } 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);
+  }
+})();

← efd7990 fix(admin/template): drag-drop upload was 403'ing on CSRF —  ·  back to NationalPaperHangers  ·  find: add sort + density controls per CLAUDE.md standing rul 72e3f64 →