← back to Lawyer Directory Builder
src/scripts/click_through.ts
132 lines
/**
* Click-through smoke test against the LIVE site at https://lawyers.agentabrams.com.
*
* Walks through the three primary flows:
* 1. Landing → "Find a lawyer" → fill form → submit → thanks
* 2. Landing → "List your firm" → /signup with autocomplete + submit
* 3. Admin login → /admin → /admin/leads → status change
*
* Screenshots written to tmp/click-through/ ; an inline report is printed.
*/
import { chromium } from 'playwright';
import fs from 'node:fs';
import path from 'node:path';
const BASE = process.env.CLICK_BASE || 'https://lawyers.agentabrams.com';
const ADMIN_EMAIL = process.env.CLICK_ADMIN_EMAIL || '';
const ADMIN_PASS = process.env.CLICK_ADMIN_PASS || '';
if (!ADMIN_EMAIL || !ADMIN_PASS) {
console.error('[click-through] CLICK_ADMIN_EMAIL and CLICK_ADMIN_PASS must be set in env');
process.exit(2);
}
const OUT = path.resolve(process.cwd(), 'tmp/click-through');
fs.mkdirSync(OUT, { recursive: true });
type Step = { name: string; ok: boolean; note?: string; shot?: string };
const steps: Step[] = [];
async function main() {
const browser = await chromium.launch({ headless: true });
try {
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
const page = await ctx.newPage();
// ── Flow 1: consumer lead funnel ─────────────────────────────────────────
try {
await page.goto(BASE + '/', { waitUntil: 'load', timeout: 20000 });
const title = await page.title();
const hasHero = await page.locator('h1').first().isVisible();
const findCta = page.locator('a[href="/find-a-lawyer"]').first();
const ctaHref = await findCta.getAttribute('href');
await page.screenshot({ path: path.join(OUT, '01-landing.png') });
steps.push({ name: 'Landing renders', ok: hasHero && title.length > 0, note: `title="${title}" cta=${ctaHref}`, shot: '01-landing.png' });
await findCta.click();
await page.waitForURL('**/find-a-lawyer', { timeout: 10000 });
await page.screenshot({ path: path.join(OUT, '02-find-form.png') });
steps.push({ name: 'Click "Find a lawyer" → /find-a-lawyer', ok: page.url().includes('/find-a-lawyer'), shot: '02-find-form.png' });
await page.fill('input[name="full_name"]', 'Smoke Test User');
await page.fill('input[name="email"]', `smoke-${Date.now()}@test.com`);
await page.fill('input[name="phone"]', '(415) 555-0100');
await page.selectOption('select[name="practice_area"]', 'personal_injury');
await page.selectOption('select[name="urgency"]', 'within_week');
await page.fill('input[name="zip"]', '90210');
await page.fill('textarea[name="case_description"]', 'Smoke test from the click-through script');
await page.screenshot({ path: path.join(OUT, '03-form-filled.png') });
await page.click('button[type="submit"]');
await page.waitForURL('**/find-a-lawyer/thanks', { timeout: 10000 });
await page.screenshot({ path: path.join(OUT, '04-thanks.png') });
steps.push({ name: 'Submit lead form → /thanks page', ok: page.url().includes('/thanks'), shot: '04-thanks.png' });
} catch (e) {
steps.push({ name: 'Lead funnel', ok: false, note: (e as Error).message.slice(0, 200) });
}
// ── Flow 2: signup form + autocomplete ───────────────────────────────────
try {
await page.goto(BASE + '/signup', { waitUntil: 'load' });
await page.screenshot({ path: path.join(OUT, '05-signup.png') });
const fields = await Promise.all([
page.locator('input[name="firm_name"]').isVisible(),
page.locator('input[name="website"]').isVisible(),
page.locator('input[name="full_name"]').isVisible(),
page.locator('input[name="email"]').isVisible(),
page.locator('input[name="phone"]').isVisible(),
]);
const allVisible = fields.every(Boolean);
steps.push({ name: '/signup shows all 5 large fields', ok: allVisible, note: `firm/site/name/email/phone visible: ${fields.join(',')}`, shot: '05-signup.png' });
// Autocomplete: type 'Emrani' and wait for dropdown
await page.fill('input#full_name', 'Emrani');
await page.waitForSelector('.ac-row', { timeout: 4000 });
const acCount = await page.locator('.ac-row').count();
await page.screenshot({ path: path.join(OUT, '06-autocomplete.png') });
await page.locator('.ac-row').first().click();
const barFilled = await page.inputValue('input#bar_number');
const firmFilled = await page.inputValue('input#firm_name');
await page.screenshot({ path: path.join(OUT, '07-autocomplete-picked.png') });
steps.push({ name: 'Autocomplete pick auto-fills bar# + firm', ok: barFilled.length > 0 && firmFilled.length > 0, note: `bar="${barFilled}" firm="${firmFilled}" rows=${acCount}`, shot: '07-autocomplete-picked.png' });
} catch (e) {
steps.push({ name: 'Signup autocomplete', ok: false, note: (e as Error).message.slice(0, 200) });
}
// ── Flow 3: admin login + leads queue ────────────────────────────────────
try {
await page.goto(BASE + '/login', { waitUntil: 'load' });
await page.fill('input[name="email"]', ADMIN_EMAIL);
await page.fill('input[name="password"]', ADMIN_PASS);
await page.click('button[type="submit"]');
await page.waitForURL(/\/dashboard/, { timeout: 10000 });
await page.screenshot({ path: path.join(OUT, '08-dashboard.png') });
steps.push({ name: 'Admin login → /dashboard', ok: page.url().includes('/dashboard'), shot: '08-dashboard.png' });
await page.goto(BASE + '/admin', { waitUntil: 'load' });
await page.screenshot({ path: path.join(OUT, '09-admin-users.png') });
const userCount = await page.locator('table tbody tr').count();
steps.push({ name: '/admin shows user table', ok: userCount > 0, note: `rows=${userCount}`, shot: '09-admin-users.png' });
await page.goto(BASE + '/admin/leads', { waitUntil: 'load' });
await page.screenshot({ path: path.join(OUT, '10-admin-leads.png') });
const leadCount = await page.locator('table tbody tr').count();
steps.push({ name: '/admin/leads shows the smoke lead', ok: leadCount > 0, note: `leads=${leadCount}`, shot: '10-admin-leads.png' });
} catch (e) {
steps.push({ name: 'Admin flow', ok: false, note: (e as Error).message.slice(0, 200) });
}
} finally {
await browser.close();
}
// ── Report ───────────────────────────────────────────────────────────────
const pass = steps.filter(s => s.ok).length;
console.log(`\n══ click-through report ══ ${pass}/${steps.length} pass\n`);
for (const s of steps) {
const mark = s.ok ? '✓' : '✗';
console.log(`${mark} ${s.name}${s.note ? ' · ' + s.note : ''}${s.shot ? ' → ' + s.shot : ''}`);
}
console.log(`\nScreenshots: ${OUT}/`);
if (pass < steps.length) process.exit(1);
}
main().catch(e => { console.error('fatal:', e); process.exit(1); });