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