← back to Scarlet Riverboat Masquerade

tests/live-happy.cjs

104 lines

// Phase 1 happy-path — REAL archive.org (no route interception), local edited index.html.
// Confirms no regression: 31 GD tiles populate with real counts (1994 ~= 460), D&C source loads.
// Run: PW=/Users/macstudio3/.npm-global/lib/node_modules/playwright node tests/live-happy.cjs
const { chromium } = require(process.env.PW);
const http = require('http');
const fs = require('fs');
const path = require('path');

const ROOT = path.resolve(__dirname, '..');
const MIME = { '.html':'text/html', '.js':'text/javascript', '.css':'text/css', '.json':'application/json', '.png':'image/png', '.jpg':'image/jpeg', '.svg':'image/svg+xml', '.ico':'image/x-icon' };
function serve() {
  return new Promise(res => {
    const srv = http.createServer((req, r) => {
      let p = decodeURIComponent(req.url.split('?')[0]);
      if (p === '/' || p === '') p = '/index.html';
      const f = path.join(ROOT, p);
      if (!f.startsWith(ROOT) || !fs.existsSync(f) || fs.statSync(f).isDirectory()) { r.writeHead(404); return r.end('nf'); }
      r.writeHead(200, { 'Content-Type': MIME[path.extname(f)] || 'application/octet-stream' });
      fs.createReadStream(f).pipe(r);
    });
    srv.listen(0, '127.0.0.1', () => res({ srv, port: srv.address().port }));
  });
}

(async () => {
  const { srv, port } = await serve();
  const base = `http://127.0.0.1:${port}/index.html`;
  const browser = await chromium.launch({ channel: 'chrome', args: ['--use-gl=swiftshader'] });
  const page = await browser.newPage();
  const errs = [];
  page.on('pageerror', e => errs.push(String(e.message || e)));
  const fails = [];
  const pass = (c, m) => { console.log((c ? 'PASS' : 'FAIL') + ' — ' + m); if (!c) fails.push(m); };

  try {
    await page.goto(base, { waitUntil: 'load', timeout: 60000 });
    await page.waitForFunction(() => document.querySelectorAll('.mr-yr').length > 0, undefined, { timeout: 30000 });
    const tiles = await page.$$eval('.mr-yr', els => els.length);
    pass(tiles === 31, `GD source renders 31 year tiles (got ${tiles})`);

    // Phase 1 DEGRADES GRACEFULLY: when archive.org is flaky the batch circuit-breaker
    // short-circuits the remaining year-count fetches (those tiles show "—", never a
    // false 0) and every good count is cached. A real visitor reloads and the cached-good
    // tiles paint instantly while the still-missing years re-fetch with a fresh breaker.
    // This harness exercises exactly that recovery — reload a bounded number of times
    // (the srm.counts.v1 cache persists across reload in this same context) until the
    // tour history is well-covered — rather than assuming one breaker-limited pass fills
    // all 31 against a nonprofit archive that is intermittently slow.
    const settleOne = async () => {
      // resolve as soon as no tile is still "loading" (breaker short-circuits fast) or
      // the whole-map error state appears; bounded so a hung archive can't stall forever.
      await page.waitForFunction(() => {
        if (document.querySelector('.mr-map-err')) return true;
        const t = [...document.querySelectorAll('.mr-yr')];
        return t.length > 0 && t.every(el => !el.classList.contains('loading'));
      }, undefined, { timeout: 45000 }).catch(() => {});
    };
    const populatedNow = () => page.$$eval('.mr-yr .yc', els => els.filter(e => /^[0-9][0-9,]*$/.test((e.textContent||'').trim()) && +(e.textContent.replace(/,/g,'')) > 0).length);
    let populated = 0;
    for (let attempt = 1; attempt <= 4; attempt++) {
      await settleOne();
      populated = await populatedNow();
      if (populated >= 28) break;
      if (attempt < 4) {
        await page.reload({ waitUntil: 'load', timeout: 60000 });
        await page.waitForFunction(() => document.querySelectorAll('.mr-yr').length > 0, undefined, { timeout: 30000 });
      }
    }

    const y1994 = await page.$eval('.mr-yr[data-y="1994"] .yc', e => (e.textContent || '').trim());
    const n1994 = parseInt(y1994.replace(/,/g, ''), 10);
    pass(n1994 >= 400 && n1994 <= 520, `1994 shows a real count ~= 460 (got ${y1994})`);

    // how many of the 31 tiles populated with a real number (across cache-backed reloads)
    pass(populated >= 28, `most GD tiles populated with real counts (${populated}/31)`);
    const zero = await page.$$eval('.mr-yr .yc', els => els.some(e => (e.textContent || '').trim() === '0'));
    pass(!zero, `no GD tile shows a false "0"`);

    // cache written for historical years
    const cacheN = await page.evaluate(() => Object.keys(JSON.parse(localStorage.getItem('srm.counts.v1') || '{}')).length);
    pass(cacheN >= 20, `historical counts cached to localStorage (${cacheN} entries)`);

    // switch to Dead & Company source
    await page.evaluate(() => { const b = document.querySelector('#mrSrc button[data-src="dc"]'); if (b) b.click(); });
    await page.waitForFunction(() => {
      const tiles = document.querySelectorAll('.mr-yr');
      return tiles.length >= 8 && [...tiles].some(t => /^[0-9][0-9,]*$/.test((t.querySelector('.yc')?.textContent||'').trim()));
    }, undefined, { timeout: 60000 });
    const dcTiles = await page.$$eval('.mr-yr', els => els.length);
    pass(dcTiles >= 8 && dcTiles <= 9, `D&C source loads (2015-2023 → ${dcTiles} tiles)`);

    pass(errs.length === 0, `no page errors: ${errs.join(' | ') || 'clean'}`);
  } catch (e) {
    fails.push('EXCEPTION: ' + (e && e.stack || e));
    console.log('EXCEPTION', e);
  } finally {
    await browser.close();
    srv.close();
  }

  console.log('\n' + (fails.length ? `RESULT: ${fails.length} FAILURE(S)` : 'RESULT: LIVE HAPPY-PATH PASS'));
  process.exit(fails.length ? 1 : 0);
})();