← back to NationalPaperHangers
tests/e2e-buyer-wizard.js
157 lines
// 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);
}
})();