← 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); });