// The Sync Test (satellites/site/index.html): errors, overflow, controls clickable, readouts filled, at 8 viewports. // CH= node sync.mjs [url] (default: the built file; LIVE=1 for https://iss-spoofing.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://iss-spoofing.aethercosmology.com/' : 'file://' + path.join(here, '../satellites/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(1500); 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]; // first line box: inline links can wrap 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)) out.blocked.push((el.id || el.textContent.trim().slice(0, 20)) + ' <- ' + (hit && (hit.id || hit.className || hit.tagName))); } for (const dd of document.querySelectorAll('.ro dd')) { if (/^—?$/.test(dd.textContent.trim())) out.empty.push(dd.closest('.panel').dataset.key + ':' + dd.dataset.f); if (dd.scrollWidth > dd.clientWidth + 1) out.clipped.push(dd.closest('.panel').dataset.key + ':' + dd.dataset.f + ' "' + dd.textContent + '"'); } for (const c of document.querySelectorAll('.map canvas')) if (!c.width) out.empty.push('canvas'); 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); await p.screenshot({ path: path.join(here, `out/sync_${w}x${h}.png`) }); await p.close(); } // behaviour: pause, seek late in orbit 1, check readouts against the data const p = await b.newPage({ viewport: { width: 1440, height: 900 } }); await p.goto(url, { waitUntil: 'load' }); await p.waitForTimeout(500); const chk = await p.evaluate(async () => { document.getElementById('play').click(); const r = document.getElementById('time'); r.value = '5000'; r.dispatchEvent(new Event('input')); await new Promise(f => setTimeout(f, 400)); const g = k => [...document.querySelectorAll(`.panel[data-key="${k}"] .ro dd`)].map(d => d.textContent); return { globe: g('globe'), ae: g('ae'), dp: g('dualpole'), me: g('mercator'), clock: document.getElementById('clock').textContent, orbit: document.getElementById('orbit').textContent, play: document.getElementById('play').getAttribute('aria-label') }; }); console.log(JSON.stringify(chk)); if (!/level/.test(chk.globe[2])) fail('globe signal not level', chk.globe[2]); if (!/behind/.test(chk.ae[2])) fail('AE signal not behind at t=5000', chk.ae[2]); if (chk.play !== 'Play') fail('pause did not toggle'); await p.screenshot({ path: path.join(here, 'out/sync_t5000.png') }); // "Line up signal now": level at once, drifts after, before the chosen moment level; "Each orbit" restores the default const lu = await p.evaluate(async () => { const r = document.getElementById('time'), wait = () => new Promise(f => setTimeout(f, 300)); const ae = () => document.querySelector('.panel[data-key="ae"] [data-f="s"]').textContent; const go = v => { r.value = String(v); r.dispatchEvent(new Event('input')); }; go(20000); await wait(); document.getElementById('sync').click(); await wait(); const at0 = ae(), info = document.getElementById('syncinfo').textContent; go(20600); await wait(); const after = ae(); go(19000); await wait(); const before = ae(); document.getElementById('syncauto').click(); go(20600); await wait(); return { at0, info, after, before, auto: ae(), infoAuto: document.getElementById('syncinfo').textContent, autoHidden: document.getElementById('syncauto').hidden }; }); console.log(JSON.stringify(lu)); if (!/level/.test(lu.at0)) fail('not level right after lining up', lu.at0); if (!/behind|ahead/.test(lu.after)) fail('no drift 10 min after lining up', lu.after); if (!/level/.test(lu.before)) fail('not level before the chosen moment', lu.before); if (!/UTC/.test(lu.info) || !/equator/.test(lu.infoAuto) || !lu.autoHidden) fail('line-up status text', lu.info, lu.infoAuto); if (lu.auto === lu.after) fail('Each orbit did not restore the default'); // "Real-time correction": the signal trails by the latency and moves at the map speed; its Doppler still says the measured speed const rc = await p.evaluate(async () => { const r = document.getElementById('time'), lat = document.getElementById('lat'), wait = () => new Promise(f => setTimeout(f, 300)); const q = (k, f) => document.querySelector(`.panel[data-key="${k}"] [data-f="${f}"]`).textContent; r.value = '5000'; r.dispatchEvent(new Event('input')); document.getElementById('corr').click(); await wait(); const l10 = q('ae', 's'), vs10 = q('ae', 'vs'), syncDisabled = document.getElementById('sync').disabled; lat.value = '0'; lat.dispatchEvent(new Event('input')); await wait(); const l0 = q('ae', 's'), vs0 = q('ae', 'vs'), v0 = q('ae', 'v'), g0 = q('globe', 'vs'); document.getElementById('corr').click(); await wait(); return { l10, vs10, syncDisabled, l0, vs0, v0, g0, off: q('ae', 's'), offvs: q('ae', 'vs'), info: document.getElementById('syncinfo').textContent }; }); console.log(JSON.stringify(rc)); if (!/^10 s behind/.test(rc.l10)) fail('correction latency 10 s not shown', rc.l10); if (!/level/.test(rc.l0) || rc.vs0 !== rc.v0) fail('zero latency: signal should be level and move at the map speed', rc.l0, rc.vs0, rc.v0); if (/×1\.00/.test(rc.vs0)) fail('corrected signal speed should differ from its Doppler on the AE map', rc.vs0); if (!/×1\.00/.test(rc.offvs) || !/×1\.00/.test(rc.g0)) fail('uncorrected / globe signal speed should equal its Doppler', rc.offvs, rc.g0); if (!rc.syncDisabled || /Corrected/.test(rc.info)) fail('line-up controls not disabled / restored', rc.syncDisabled, rc.info); await b.close(); console.log(fails ? `${fails} failures` : 'sync OK'); process.exit(fails ? 1 : 0);