← back to Paul Conrad Cartoons
generator/verify-contact-sheet.cjs
56 lines
// verify-contact-sheet.cjs — headless-Chromium E2E check of generator/out/index.html.
// Asserts: every manifest item renders as a card, every <img> actually decoded
// (naturalWidth > 0), every card shows a created date/time chip, sort + density
// controls work and persist across reload, zero console errors. Then screenshots
// to generator/out/contact-sheet.png. Exit 1 on any failed assertion.
// Negative self-test: `node generator/verify-contact-sheet.cjs --test-broken`
// points at a copy with a missing image and MUST exit 1.
const path = require('path');
const fs = require('fs');
const { chromium } = require(process.env.PW || '/Users/macstudio3/Projects/animals/node_modules/playwright');
const OUT = path.join(__dirname, 'out');
(async () => {
const broken = process.argv.includes('--test-broken');
let page_path = path.join(OUT, 'index.html');
if (broken) {
const tmp = fs.mkdtempSync(path.join(require('os').tmpdir(), 'sm-neg-'));
fs.writeFileSync(path.join(tmp, 'index.html'), fs.readFileSync(page_path, 'utf8')); // no PNGs next to it
page_path = path.join(tmp, 'index.html');
}
const manifest = JSON.parse(fs.readFileSync(path.join(OUT, 'manifest.json'), 'utf8'));
const browser = await chromium.launch();
const ctx = await browser.newContext({ viewport: { width: 1440, height: 1100 } });
const page = await ctx.newPage();
const errors = [];
page.on('console', m => { if (m.type() === 'error') errors.push(m.text()); });
page.on('pageerror', e => errors.push(String(e)));
await page.goto('file://' + page_path);
await page.waitForLoadState('networkidle');
await page.evaluate(async () => { for (const i of document.images) { i.loading = 'eager'; if (!i.complete) await new Promise(r => { i.onload = i.onerror = r; }); } });
const fails = [];
const cards = await page.locator('.card').count();
if (cards !== manifest.items.length) fails.push(`cards ${cards} != manifest ${manifest.items.length}`);
const badImgs = await page.evaluate(() => [...document.images].filter(i => !i.naturalWidth).length);
if (badImgs) fails.push(`${badImgs} images failed to decode`);
const whens = await page.locator('.when').count();
if (whens !== cards) fails.push(`date chips ${whens} != cards ${cards}`);
const firstWhen = await page.locator('.when').first().textContent().catch(() => '');
if (!/\d{1,2}:\d{2}/.test(firstWhen || '')) fails.push(`date chip lacks time: ${firstWhen}`);
// sort + density persistence
await page.selectOption('#sort', 'caption');
await page.locator('#density').fill('420');
await page.reload(); await page.waitForLoadState('networkidle');
const sortV = await page.inputValue('#sort'), densV = await page.inputValue('#density');
if (sortV !== 'caption' || densV !== '420') fails.push(`persistence failed sort=${sortV} density=${densV}`);
// reset to defaults for the screenshot
await page.selectOption('#sort', 'newest'); await page.locator('#density').fill('300');
await page.evaluate(async () => { for (const i of document.images) { i.loading = 'eager'; if (!i.complete) await new Promise(r => { i.onload = i.onerror = r; }); } });
if (errors.filter(e => !broken || !/ERR_FILE_NOT_FOUND/.test(e)).length) fails.push(`console errors: ${errors.join(' | ')}`);
if (!broken) await page.screenshot({ path: path.join(OUT, 'contact-sheet.png'), fullPage: true });
await browser.close();
const result = { verdict: fails.length ? 'FAIL' : 'PASS', cards, bad_images: badImgs, date_chips: whens, first_date_chip: firstWhen, persisted: { sort: sortV, density: densV }, console_errors: errors.length, fails, mode: broken ? 'negative-test' : 'real' };
console.log(JSON.stringify(result, null, 1));
process.exit(fails.length ? 1 : 0);
})().catch(e => { console.error(e); process.exit(1); });