01ffa67de6
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
117 lines
7.8 KiB
JavaScript
117 lines
7.8 KiB
JavaScript
// 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=<chromium> 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.waitForTimeout(500);
|
|
const ch = await p.evaluate(() => [document.getElementById('status').textContent, sv.total, running]);
|
|
if (!/^Measurements changed/.test(ch[0]) || ch[1] !== 0 || ch[2]) fail('changing the data should re-score without moving the cities', ch);
|
|
await p.click('#run');
|
|
await p.waitForFunction(() => /^Done/.test(document.getElementById('status').textContent), null, { timeout: 120000 });
|
|
const kf = await broken();
|
|
const capf = await p.evaluate(() => document.getElementById('mapcap').textContent);
|
|
if (!/no flat map can fail them/.test(capf)) fail('flight-times-off caption missing', capf);
|
|
console.log('Limits only: broken', kf);
|
|
if (kf > 5) fail('limits alone should be (nearly) satisfiable on a plane', kf);
|
|
await p.click('#d-fl');
|
|
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') });
|
|
// measured along a sphere (assumes the shape, leaves its size free)
|
|
await p.click('#arc');
|
|
await p.waitForFunction(() => document.getElementById('r3s').textContent === 'done', null, { timeout: 180000 });
|
|
const ra = await p.evaluate(() => ({ b: +document.getElementById('r3b').textContent.replace(/,/g, ''), r: +document.getElementById('r3r').textContent.replace(/[^\d]/g, ''),
|
|
o: +document.getElementById('r3o').textContent.replace(/[^\d]/g, ''), note: document.getElementById('note3').textContent }));
|
|
console.log('Along a sphere', JSON.stringify({ b: ra.b, r: ra.r, o: ra.o }));
|
|
if (!(ra.r > 6200 && ra.r < 6700)) fail('radius along a sphere out of range', ra.r);
|
|
if (!(ra.b <= 5) || !(ra.o > 0 && ra.o < 1000)) fail('sphere fit poor', ra);
|
|
if (!/haversine/.test(ra.note) || !/Assumes a sphere/.test(ra.note)) fail('sphere caveat missing', ra.note);
|
|
await p.locator('.lift').screenshot({ path: path.join(here, 'out/femap_arc.png') });
|
|
await p.click('#arc');
|
|
await p.waitForFunction(() => document.getElementById('r3s').textContent === 'done', null, { timeout: 180000 });
|
|
if (!/No shape is assumed/.test(await p.evaluate(() => document.getElementById('note3').textContent))) fail('free-mode note not restored');
|
|
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);
|