// The Best Flat Map (femap/site/index.html): errors, overflow, controls clickable, score filled, at 8 viewports; // then the solver: it reduces the broken count from the North-pole AE start, data toggles change the count, // and the 3D release closes round. // CH= node femap.mjs [url] (default: the built file; LIVE=1 for https://fe-map.aethercosmology.com) import { chromium } from 'playwright-core'; import { fileURLToPath } from 'url'; import path from 'path'; const here = path.dirname(fileURLToPath(import.meta.url)); const url = process.argv[2] || (process.env.LIVE ? 'https://fe-map.aethercosmology.com/' : 'file://' + path.join(here, '../femap/site/index.html')); const VP = [[1920, 1080], [1680, 1050], [1440, 900], [1280, 720], [1024, 768], [820, 1180], [390, 844], [360, 740]]; const b = await chromium.launch({ executablePath: process.env.CH }); let fails = 0; const fail = (...a) => { fails++; console.log('FAIL', ...a); }; for (const [w, h] of VP) { const p = await b.newPage({ viewport: { width: w, height: h }, deviceScaleFactor: 1 }); const errs = []; p.on('pageerror', e => errs.push(e.message)); p.on('console', m => m.type() === 'error' && errs.push(m.text())); await p.goto(url, { waitUntil: 'load' }); await p.waitForTimeout(2500); const r = await p.evaluate(() => { const out = { overflow: document.documentElement.scrollWidth - innerWidth, blocked: [], empty: [], clipped: [] }; for (const el of document.querySelectorAll('button, input, a')) { el.scrollIntoView({ block: 'center' }); const b = el.getClientRects()[0]; if (!b || !b.width) continue; const hit = document.elementFromPoint(b.left + b.width / 2, b.top + b.height / 2); if (hit !== el && !el.contains(hit) && !(el.tagName === 'INPUT' && hit && hit.closest('label') === el.closest('label'))) out.blocked.push((el.id || el.textContent.trim().slice(0, 20)) + ' <- ' + (hit && (hit.id || hit.className || hit.tagName))); } if (document.querySelectorAll('#score tr').length < 6) out.empty.push('score'); if (!document.querySelector('#bands tbody tr')) out.empty.push('bands'); for (const c of document.querySelectorAll('canvas')) if (!c.width || !c.height) out.empty.push('canvas ' + c.id); for (const td of document.querySelectorAll('#score td')) if (td.scrollWidth > td.clientWidth + 1) out.clipped.push(td.textContent); for (const el of document.querySelectorAll('.worst b')) if (el.scrollWidth > el.clientWidth + 1 && el.closest('li').getBoundingClientRect().width < 120) out.clipped.push(el.textContent); window.scrollTo(0, 0); return out; }); if (errs.length) fail(w, h, 'errors', errs.slice(0, 3)); if (r.overflow > 0) fail(w, h, 'horizontal overflow', r.overflow); if (r.blocked.length) fail(w, h, 'blocked', r.blocked.slice(0, 5)); if (r.empty.length) fail(w, h, 'empty', r.empty); if (r.clipped.length) fail(w, h, 'clipped', r.clipped.slice(0, 4)); await p.screenshot({ path: path.join(here, `out/femap_${w}x${h}.png`) }); await p.close(); } // behaviour const p = await b.newPage({ viewport: { width: 1440, height: 900 } }); const errs = []; p.on('pageerror', e => errs.push(e.message)); await p.goto(url, { waitUntil: 'load' }); const broken = () => p.evaluate(() => +(document.getElementById('status').textContent.match(/broken ([\d,]+)/) || [0, 'NaN'])[1].replace(/,/g, '')); await p.waitForTimeout(300); const k0 = await broken(); await p.waitForFunction(() => /^Done/.test(document.getElementById('status').textContent), null, { timeout: 120000 }); const k1 = await broken(); console.log('AE start: broken', k0, '->', k1); if (!(k1 < k0)) fail('solver did not reduce the broken count', k0, k1); if (k1 > 60) fail('live solve far from the offline best', k1); await p.screenshot({ path: path.join(here, 'out/femap_solved.png') }); // best of N: loads without solving and matches the build await p.click('#starts button[data-start="best"]'); await p.waitForTimeout(400); const kb = await broken(); const want = await p.evaluate(() => D.stats.best.broken); console.log('Best start: broken', kb, 'build says', want); if (kb !== want) fail('best layout count differs from the build', kb, want); // flights off: the plane can satisfy limits alone by shrinking await p.click('#d-fl'); await p.waitForFunction(() => /^Done/.test(document.getElementById('status').textContent), null, { timeout: 120000 }); const kf = await broken(); console.log('Limits only: broken', kf); if (kf > 5) fail('limits alone should be (nearly) satisfiable on a plane', kf); if (await p.evaluate(() => document.getElementById('flnote').hidden)) fail('flight-times-off note not shown'); await p.click('#d-fl'); if (!(await p.evaluate(() => document.getElementById('flnote').hidden))) fail('flight-times-off note still shown'); await p.click('#starts button[data-start="best"]'); await p.waitForTimeout(300); // 3D release await p.click('#lift'); await p.waitForFunction(() => document.getElementById('r3s').textContent === 'done', null, { timeout: 180000 }); const r3 = await p.evaluate(() => ({ b: document.getElementById('r3b').textContent, w: document.getElementById('r3w').textContent, r: document.getElementById('r3r').textContent, f: document.getElementById('r3f').textContent })); console.log('3D', JSON.stringify(r3)); if (!(parseInt(r3.w) > 120)) fail('3D release did not wrap round', r3.w); if (!(+r3.b.replace(/,/g, '') < +r3.f.replace(/,/g, ''))) fail('3D not better than the plane', r3); await p.locator('.lift').screenshot({ path: path.join(here, 'out/femap_3d.png') }); if (errs.length) fail('errors', errs.slice(0, 3)); console.log(fails ? `femap: ${fails} FAILS` : 'femap OK'); await b.close(); process.exit(fails ? 1 : 0);