← back to Ventura Corridor

scripts/audit-tabs.cjs

124 lines

// Click-through audit of every tab on the ventura-corridor app.
// For each tab: load it, snapshot screenshot, count meaningful content
// markers (non-empty grids, filled tables, populated cards), report
// "POPULATED" vs "EMPTY" vs "ERROR" so we know which need demo data.

const { chromium } = require('playwright-core');
const fs = require('fs');
const path = require('path');

const BASE = process.env.BASE || 'http://127.0.0.1:9780';
const SHOTS_DIR = path.join(__dirname, '..', 'tmp', 'audit-shots');
fs.mkdirSync(SHOTS_DIR, { recursive: true });

// Tabs explicitly linked from the main nav (these are the "tabs" the user
// would visit); plus all top-level page-pages.
const TABS = [
  '/', '/atlas.html', '/chains.html', '/dw-radar.html', '/eulogy.html',
  '/headlines.html', '/linkedin.html', '/pantone.html', '/pitches.html',
  '/signals.html', '/timeline.html', '/today.html', '/tongues.html',
  '/wall.html',
  // Secondary pages
  '/buildings.html', '/calendar.html', '/coverage.html', '/duplicates.html',
  '/find.html', '/scoreboard.html', '/responses.html', '/walk-route.html',
  '/postcards.html', '/sponsor.html', '/rate-card.html', '/today.html',
  '/magazine.html', '/headlines.html'
];
const seen = new Set();
const TABS_UNIQ = TABS.filter(t => { if (seen.has(t)) return false; seen.add(t); return true; });

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 { fs.accessSync(p); return p; } catch {} }
  return null;
}

(async () => {
  const exe = findChromium();
  if (!exe) { console.error('FAIL: no Chromium'); process.exit(2); }
  const browser = await chromium.launch({ executablePath: exe, headless: true });
  const ctx = await browser.newContext({
    viewport: { width: 1440, height: 900 },
    httpCredentials: {
      username: process.env.BASIC_AUTH_USER || 'admin',
      password: process.env.BASIC_AUTH_PASS || 'DWSecure2024!'
    }
  });
  const page = await ctx.newPage();

  const results = [];
  for (const tab of TABS_UNIQ) {
    const url = BASE + tab;
    process.stdout.write(`  ${tab.padEnd(28)} `);
    let status = 'ERROR', count = 0, marker = '', err = '';
    try {
      const resp = await page.goto(url, { waitUntil: 'networkidle', timeout: 12000 });
      const code = resp ? resp.status() : 0;
      if (code !== 200) {
        status = `HTTP_${code}`;
      } else {
        // Wait a beat for any deferred fetch() to populate the DOM
        await page.waitForTimeout(800);
        // Heuristics for "has content"
        const counts = await page.evaluate(() => {
          const get = sel => document.querySelectorAll(sel).length;
          return {
            tableRows: get('tbody tr'),
            cards: get('.card, .item, .entry, .row, .bldg, .quote, article'),
            dataRows: get('[data-id], [data-addr], [data-business-id], [data-slug], [data-bldg]'),
            listItems: get('ul li:not(.empty):not(.loading), ol li'),
            mapMarkers: get('.leaflet-marker-icon, .leaflet-interactive'),
            svgPaths: get('svg path, svg circle, svg rect'),
            anyContent: get('main, section')
          };
        });
        const total = counts.tableRows + counts.cards + counts.dataRows + counts.listItems + counts.mapMarkers;
        count = total;
        // Explicit empty-state text markers
        const txt = (await page.textContent('body') || '').slice(0, 4000).toLowerCase();
        const emptyMarker = (
          /no (data|results|business|signal|pitch|response|chain|event|post|item)/i.test(txt) ||
          /loading…|loading\.\.\./i.test(txt.slice(0, 1200)) ||
          /coming soon|nothing here yet/i.test(txt)
        );
        // 1 demo row counts as populated per "ok to use 1 demo record"
        if (total >= 1) status = 'POPULATED';
        else if (counts.svgPaths >= 5) status = 'POPULATED';  // SVG-only viz tabs
        else if (emptyMarker) status = 'EMPTY';
        else if (counts.anyContent > 0) status = 'PARTIAL';
        else status = 'EMPTY';
        marker = JSON.stringify(counts);
      }
      // Save screenshot for visual review
      const shotName = tab.replace(/\W+/g, '_').replace(/^_+|_+$/g, '') || 'index';
      try { await page.screenshot({ path: path.join(SHOTS_DIR, `${shotName}.png`), fullPage: false }); } catch {}
    } catch (e) {
      err = e.message.slice(0, 80);
      status = 'EXCEPTION';
    }
    const tag = status === 'POPULATED' ? '✓'
      : status === 'EMPTY' ? '⊘'
      : status === 'PARTIAL' ? '~'
      : '✗';
    console.log(`${tag} ${status.padEnd(11)} ${count} content markers ${marker || err}`);
    results.push({ tab, status, count, marker, err });
  }

  // Summary
  const by = {};
  results.forEach(r => { by[r.status] = (by[r.status] || 0) + 1; });
  console.log('\nSummary:');
  for (const k of Object.keys(by)) console.log(`  ${k}: ${by[k]}`);

  fs.writeFileSync(path.join(__dirname, '..', 'tmp', 'audit-tabs.json'),
    JSON.stringify(results, null, 2));
  console.log(`\nFull results: ${path.join(__dirname, '..', 'tmp', 'audit-tabs.json')}`);
  console.log(`Screenshots: ${SHOTS_DIR}/`);

  await browser.close();
})();