← 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
A tests/e2e-buyer-wizard.js
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 →