← back to Paul Conrad Cartoons Rebrand

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