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