← back to NationalPaperHangers
tests/e2e-coi-request.js
179 lines
// e2e click-test: Live COI request flow (UX backlog #5).
//
// Drives the end-to-end designer flow + admin inbox:
// 1. Public /installer/:slug — "Request COI →" button visible (gated on
// insurance_on_file=true).
// 2. Reveal form, fill required fields, submit → /coi-request-result page
// renders ok.
// 3. DB: row exists in coi_requests for the installer, status='pending'.
// 4. Login as the installer → /admin/coi-requests shows the request.
// 5. Mark fulfilled → DB row status='fulfilled', fulfilled_at set.
// 6. Cleanup — delete the test request row.
//
// Run: node tests/e2e-coi-request.js
// Env: BASE=http://localhost:9765 (default)
// SLUG=atelier-bond-nyc (default — must be a claimed installer with
// insurance_on_file=true)
// EMAIL=demo+bond@example.com (matching SLUG)
// PW=demo1234
const path = require('path');
require('dotenv').config({ path: path.resolve(__dirname, '..', '.env') });
const { chromium } = require('playwright-core');
const BASE = process.env.BASE || 'http://localhost:9765';
const SLUG = process.env.SLUG || 'atelier-bond-nyc';
const EMAIL = process.env.EMAIL || 'demo+bond@example.com';
const PW = process.env.PW || 'demo1234';
const IS_LOCAL = /localhost|127\.0\.0\.1/.test(BASE);
if (!IS_LOCAL) {
console.log(`[e2e-coi-request] SKIP — BASE=${BASE} is remote; this test writes to DB and verifies via SQL`);
process.exit(78);
}
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;
}
let _db = null;
function db() {
if (_db) return _db;
_db = require('../lib/db');
return _db;
}
const DESIGNER_NAME = `E2E Test Designer ${Date.now()}`;
const DESIGNER_EMAIL = `e2e-test-${Date.now()}@example.com`;
const ADDITIONAL_INSURED = 'ACME Design LLC, its officers and directors';
(async () => {
const exe = findChromium();
if (!exe) { console.error('FAIL: no Chromium binary'); process.exit(2); }
console.log(`[e2e-coi] 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++; } };
let createdRequestId = null;
try {
// ─── Step 1 — Public profile renders the COI section ────────────────
console.log(`\n[step 1] public /installer/${SLUG} renders COI section`);
await page.goto(`${BASE}/installer/${SLUG}`, { waitUntil: 'domcontentloaded' });
const coiSection = await page.$('#coi-section');
assert(!!coiSection, 'COI section #coi-section present');
const btn = await page.$('[data-coi-toggle]');
assert(!!btn, '"Request COI →" button present');
// ─── Step 2 — Reveal form, fill, submit ─────────────────────────────
console.log(`\n[step 2] reveal form + submit request`);
await page.click('[data-coi-toggle]');
await page.waitForSelector('input[name="designer_name"]', { timeout: 3000 });
await page.fill('input[name="designer_name"]', DESIGNER_NAME);
await page.fill('input[name="designer_email"]', DESIGNER_EMAIL);
await page.fill('input[name="designer_company"]', 'E2E Test Firm');
await page.fill('input[name="project_name"]', 'E2E Test Suite');
await page.fill('input[name="project_address"]', '123 Test Ave, Beverly Hills CA 90210');
await page.fill('input[name="additional_insured_name"]', ADDITIONAL_INSURED);
await page.fill('input[name="additional_insured_address"]', '456 Design Blvd, LA CA 90069');
await Promise.all([
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 8000 }),
page.click('#coi-request-form button[type="submit"]')
]);
const url = page.url();
assert(/\/installer\/.*\/coi-request/.test(url) || /coi-request-result/.test(url) || url.includes(`/installer/${SLUG}/coi-request`),
`landed on COI result page (got ${url})`);
const body = await page.content();
assert(/COI request sent|in flight|received/i.test(body), 'result page shows confirmation copy');
// ─── Step 3 — DB row landed ─────────────────────────────────────────
console.log(`\n[step 3] DB row created`);
const rows = await db().many(
`SELECT cr.id, cr.status, cr.designer_name, cr.designer_email, cr.additional_insured_name
FROM coi_requests cr
JOIN installers i ON i.id = cr.installer_id
WHERE i.slug = $1 AND cr.designer_email = $2
ORDER BY cr.created_at DESC LIMIT 1`,
[SLUG, DESIGNER_EMAIL]
);
assert(rows.length === 1, `exactly 1 coi_requests row for this designer (got ${rows.length})`);
if (rows.length) {
createdRequestId = rows[0].id;
assert(rows[0].status === 'pending', `status=pending (got "${rows[0].status}")`);
assert(rows[0].designer_name === DESIGNER_NAME, `designer_name matches`);
assert(rows[0].additional_insured_name === ADDITIONAL_INSURED, `additional_insured_name matches`);
}
// ─── Step 4 — Login as installer and verify /admin/coi-requests ─────
console.log(`\n[step 4] login as ${EMAIL} → /admin/coi-requests shows request`);
const loginResp = await page.goto(`${BASE}/login`, { waitUntil: 'domcontentloaded' });
if (loginResp && loginResp.status() === 429) {
console.log(' ⊘ login rate-limit (429) — skipping admin verification phases');
} else {
await page.fill('input[name="email"]', EMAIL);
await page.fill('input[name="password"]', PW);
await Promise.all([
page.waitForURL(/\/admin/, { timeout: 8000 }),
page.click('form[action="/login"] button[type="submit"]')
]);
assert(page.url().includes('/admin'), 'logged in');
await page.goto(`${BASE}/admin/coi-requests`, { waitUntil: 'domcontentloaded' });
const inboxHtml = await page.content();
assert(inboxHtml.includes(DESIGNER_NAME), 'admin inbox shows new request');
assert(/pending/i.test(inboxHtml), 'request shows as pending');
// ─── Step 5 — Mark fulfilled ──────────────────────────────────────
console.log(`\n[step 5] mark request fulfilled`);
if (createdRequestId) {
// Find the form for this specific request and submit "Mark fulfilled"
const formSel = `form[action="/admin/coi-requests/${createdRequestId}/status"] button[value="fulfilled"]`;
const fulfillBtn = await page.$(formSel);
assert(!!fulfillBtn, '"Mark fulfilled" button present');
if (fulfillBtn) {
await Promise.all([
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 5000 }),
page.click(formSel)
]);
const after = await db().one(
`SELECT status, fulfilled_at FROM coi_requests WHERE id=$1`,
[createdRequestId]
);
assert(after && after.status === 'fulfilled', `DB status flipped to fulfilled (got "${after && after.status}")`);
assert(after && after.fulfilled_at, 'fulfilled_at timestamp set');
}
}
}
} catch (err) {
console.error(`\nFAIL — uncaught: ${err.message}`);
fail++;
} finally {
// ─── Cleanup ─────────────────────────────────────────────────────────
if (createdRequestId) {
try {
await db().query(`DELETE FROM coi_requests WHERE id = $1`, [createdRequestId]);
console.log(`\n[cleanup] deleted coi_requests id=${createdRequestId}`);
} catch (e) {
console.error(`[cleanup] failed: ${e.message}`);
}
}
await browser.close();
}
console.log(`\n[result] ${pass} pass · ${fail} fail`);
process.exit(fail > 0 ? 1 : 0);
})();