// 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(600);
const k0 = await broken();
const st0 = await p.evaluate(() => [document.getElementById('status').textContent, document.getElementById('mapcap').textContent]);
if (!/as drawn/.test(st0[0]) || !/Press Solve/.test(st0[1])) fail('page should open paused on the starting map', st0);
await p.waitForTimeout(600);
if (await broken() !== k0) fail('starting map changed before Solve was pressed');
await p.click('#run');
await p.waitForTimeout(2000);
const moving = await p.evaluate(() => sv.total);
if (!(moving > 0 && moving < 400)) fail('solve should start slowly so the move can be followed', moving);
await p.waitForFunction(() => /^Done/.test(document.getElementById('status').textContent), null, { timeout: 120000 });
const k1 = await broken();
const cap1 = await p.evaluate(() => document.getElementById('mapcap').textContent);
if (!/^Santiago: \d+ of \d+ flights kept/.test(cap1)) fail('Santiago should be selected when the solve ends', cap1);
console.log('AE start: broken', k0, '->', k1, '|', cap1);
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);