ad8b889437
- femap/: planar best-effort solver, build, result and page (fe-map.aethercosmology.com) - satellites/: The Sync Test build, ISS elements, SatNOGS timings and page (iss.aethercosmology.com) - src/handoff/The_Shape_of_Distance_PRESENTATION.html: inputs for both - CONTRIBUTING.md; README and LICENSE-DATA cover all four sites; GitHub mirror for issues and pull requests Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
100 lines
6.8 KiB
JavaScript
100 lines
6.8 KiB
JavaScript
// The Sync Test (satellites/site/index.html): errors, overflow, controls clickable, readouts filled, at 8 viewports.
|
||
// CH=<chromium> node sync.mjs [url] (default: the built file; LIVE=1 for https://iss.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.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);
|