Files
cable-latency-survey/tests/sync.mjs
T
Toby ad8b889437 Add fe-map and iss sources, The Shape of Distance as published, contribution guide
- 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>
2026-10-30 18:42:34 -04:00

100 lines
6.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);