Open-source release: pipeline code, raw logs, derived data, licences, tests

Adds src/ (scripts, raw measurement logs, derived data, report source),
tests/, LICENSE (MIT, code) and LICENSE-DATA.md (CC BY 4.0 for the survey's
data; third-party terms). The report is updated to the current site; the
v1 and v2 data hand-off files are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Toby
2026-10-27 20:09:54 -04:00
parent 75da69fc31
commit c094eb88fa
81 changed files with 249210 additions and 127 deletions
+99
View File
@@ -0,0 +1,99 @@
import { chromium } from 'playwright-core';
import { PAGE, HAND } from './paths.mjs';
const URL = PAGE('web/index.html');
const VPS = [[1920,1080],[1440,900],[1366,768],[1280,720],[1024,768],[820,1180],[390,844],[360,740]];
const b = await chromium.launch({ executablePath: process.env.CH });
const problems = [], errs = [];
const audit = async (p, tag) => p.evaluate((tag) => {
const out = [], vw = innerWidth, wideM = vw > 900;
if (document.documentElement.scrollWidth > vw + 1) out.push(`${tag}: page scrolls sideways (${document.documentElement.scrollWidth} > ${vw})`);
const hero = document.getElementById('hero').getBoundingClientRect();
// every visible control in the hero must be the element actually under its centre
for (const el of document.querySelectorAll('#hero button, #hero select, #hero .li, #hero .tabs button')) {
const r = el.getBoundingClientRect(); if (!r.width || !r.height) continue;
const cx = r.left + r.width / 2, cy = r.top + r.height / 2;
if (cy < 0 || cy > innerHeight || cx < 0 || cx > vw) continue; // off-screen (scroll), not covered
let clipped = false; // scrolled out of view inside a scrolling box
for (let a = el.parentElement; a && a !== document.body; a = a.parentElement) {
const o = getComputedStyle(a).overflowY; if (o === 'visible') continue;
const q = a.getBoundingClientRect(); if (cy < q.top || cy > q.bottom || cx < q.left || cx > q.right) { clipped = true; break; } }
if (clipped) continue;
const top = document.elementFromPoint(cx, cy);
if (!top || !(el === top || el.contains(top))) out.push(`${tag}: "${(el.textContent||el.getAttribute('aria-label')||'').trim().slice(0,30)}" covered by ${top ? (top.id || top.className || top.tagName) : 'nothing'}`);
if (el.scrollWidth > el.clientWidth + 1 && el.tagName === 'BUTTON') out.push(`${tag}: text clipped in button "${el.textContent.trim().slice(0,30)}"`);
}
for (const el of document.querySelectorAll('#card, #card .body, #card .row, #card .li, .legend, .tools, .models'))
if (el.scrollWidth > el.clientWidth + 1) out.push(`${tag}: content wider than its box in ${el.id || el.className} (${el.scrollWidth} > ${el.clientWidth})`);
if (wideM) {
const boxes = ['.tl', '.bl', '.models', '#card', '#legend', '.tools'].map(s => [s, document.querySelector(s).getBoundingClientRect()]);
for (let i = 0; i < boxes.length; i++) {
const [n, r] = boxes[i];
if (r.left < hero.left - 1 || r.right > hero.right + 1 || r.top < hero.top - 1 || r.bottom > hero.bottom + 1) out.push(`${tag}: ${n} sticks out of the hero`);
for (let j = i + 1; j < boxes.length; j++) { const [m, q] = boxes[j];
const ox = Math.min(r.right, q.right) - Math.max(r.left, q.left), oy = Math.min(r.bottom, q.bottom) - Math.max(r.top, q.top);
if (ox > 1 && oy > 1) out.push(`${tag}: ${n} overlaps ${m} (${Math.round(ox)}x${Math.round(oy)})`); }
}
// the map itself against the HUD
const t = T[S.view], cv = document.getElementById('cv').getBoundingClientRect(), cx = cv.left + t.x, cy = cv.top + t.y;
const rad = S.view === 'globe' ? t.k : Math.PI * t.k;
for (const [n, r] of boxes) {
let hit;
if (S.view === 'merc') hit = cx - rad < r.right && cx + rad > r.left && cy - rad < r.bottom && cy + rad > r.top;
else { const dx = Math.max(r.left - cx, 0, cx - r.right), dy = Math.max(r.top - cy, 0, cy - r.bottom); hit = dx * dx + dy * dy < rad * rad; }
if (hit) out.push(`${tag}: map (${S.view}) runs under ${n}`);
}
if (cy - rad < hero.top - 1 || cy + rad > hero.bottom + 1 || cx - rad < hero.left - 1) out.push(`${tag}: map cut off by the hero edge`);
out.push(`INFO ${tag}: map radius ${Math.round(rad)}px of hero ${Math.round(hero.width)}x${Math.round(hero.height)}; card ${Math.round(document.getElementById('card').getBoundingClientRect().height)}px tall, body scrolls: ${document.querySelector('#card .body').scrollHeight > document.querySelector('#card .body').clientHeight}`);
}
return out;
}, tag);
for (const [w, h] of VPS) {
const p = await b.newPage({ viewport: { width: w, height: h } });
p.on('console', m => { if (m.type() === 'error') errs.push(`${w}x${h} ${m.text()}`); });
p.on('pageerror', e => errs.push(`${w}x${h} pageerror ${e.message}`));
await p.goto(URL, { waitUntil: 'load' }); await p.waitForTimeout(2500);
const tag = `${w}x${h}`;
problems.push(...await audit(p, `${tag} route/ae`));
await p.screenshot({ path: `out/${tag}_route.png` });
await p.click('#rbig'); await p.waitForTimeout(300);
problems.push(...await audit(p, `${tag} list/ae`));
await p.screenshot({ path: `out/${tag}_list.png` });
for (const v of ['globe', 'dp', 'merc']) {
await p.click(`.models button[data-v="${v}"]`); await p.waitForTimeout(1200);
problems.push(...await audit(p, `${tag} list/${v}`));
}
await p.click('#mode button[data-mode="line"]'); await p.waitForTimeout(300);
problems.push(...await audit(p, `${tag} list/merc/line`));
await p.click('.models button[data-v="ae"]'); await p.waitForTimeout(1200);
// interactions: list row selects, toggles respond
const before = await p.evaluate(() => S.sel);
const rows = await p.$$('#card .li');
if (rows.length > 2) { await rows[2].click(); await p.waitForTimeout(200); }
const after = await p.evaluate(() => S.sel);
if (rows.length > 2 && before === after) problems.push(`${tag}: clicking a list row did not select it`);
await p.click('#tbad'); await p.click('#tpulse'); await p.waitForTimeout(150);
const st = await p.evaluate(() => [S.onlyBad, S.pulses, document.getElementById('rbig').textContent, document.querySelectorAll('#card .li').length]);
if (!st[0] || st[1]) problems.push(`${tag}: toggles did not change state ${JSON.stringify(st)}`);
problems.push(`INFO ${tag}: counter ${st[2]} vs list rows ${st[3]} (line mode, AE)`);
if (w === 1440) await p.screenshot({ path: `out/${tag}_line_onlybad.png` });
await p.close();
}
// page body + form at desktop and phone
for (const [w, h] of [[1440, 900], [390, 844]]) {
const p = await b.newPage({ viewport: { width: w, height: h } });
p.on('pageerror', e => errs.push(`${w} ${e.message}`));
await p.goto(URL, { waitUntil: 'load' }); await p.waitForTimeout(1500);
await p.fill('#byoA', 'Perth, AU'); await p.selectOption('#byoB', { index: 0 }); await p.fill('#byoT', '48');
await p.click('button.go'); await p.waitForTimeout(200);
problems.push(`INFO ${w} form: ` + (await p.$eval('#byoout .verdict', e => e.textContent)).replace(/\s+/g, ' '));
await p.fill('#byoA', 'Nowhereville'); await p.click('button.go'); await p.waitForTimeout(100);
problems.push(`INFO ${w} bad city: ` + (await p.$eval('#byoout', e => e.textContent)).replace(/\s+/g, ' ').slice(0, 90));
const sw = await p.evaluate(() => document.documentElement.scrollWidth);
if (sw > w + 1) problems.push(`${w}: page scrolls sideways ${sw}`);
await p.screenshot({ path: `out/full_${w}.png`, fullPage: true });
await p.close();
}
console.log('ERRORS', JSON.stringify(errs));
for (const x of problems) console.log(x);
await b.close();
+16
View File
@@ -0,0 +1,16 @@
import { chromium } from 'playwright-core';
import { PAGE, HAND } from './paths.mjs';
// every route selected in turn, on every map: the route panel must never overflow sideways
const b = await chromium.launch({ executablePath: process.env.CH });
const bad = [];
for (const [w, h] of [[1440, 900], [1024, 768], [390, 844]]) {
const p = await b.newPage({ viewport: { width: w, height: h } });
await p.goto(PAGE('web/index.html'), { waitUntil: 'load' }); await p.waitForTimeout(2500);
for (const v of ['globe', 'ae', 'dp', 'merc']) {
await p.click(`.models button[data-v="${v}"]`); await p.waitForTimeout(1100);
const r = await p.evaluate(() => { const out = []; for (const q of LP) { select(q.idx); for (const el of document.querySelectorAll('#card .body, #card .row')) if (el.scrollWidth > el.clientWidth + 1) { out.push(`${LS[q.a].name}-${LS[q.b].name} ${el.className} ${el.scrollWidth}>${el.clientWidth}`); break } } return out; });
if (r.length) bad.push(`${w}x${h} ${v}: ${r.length} routes overflow, e.g. ${r[0]}`);
}
await p.close();
}
console.log('CARD OVERFLOW', bad.length ? bad.join('\n') : 'none'); await b.close();
+11
View File
@@ -0,0 +1,11 @@
import { chromium } from 'playwright-core';
import { PAGE, HAND } from './paths.mjs';
const b = await chromium.launch({ executablePath: process.env.CH });
for (const [w, h] of [[1920,1080],[1440,900],[1366,768],[1280,720],[1024,768]]) {
const p = await b.newPage({ viewport: { width: w, height: h } });
await p.goto(PAGE('web/index.html'), { waitUntil: 'load' }); await p.waitForTimeout(1300);
const r = await p.evaluate(() => { const e = document.querySelector('#card .body'); return e.scrollHeight - e.clientHeight; });
console.log(`${w}x${h}: route panel overflow ${r}px`);
await p.close();
}
await b.close();
+24
View File
@@ -0,0 +1,24 @@
// Load the public site in a browser at desktop and phone size: no errors, counts equal the hand-off data file.
import { chromium } from 'playwright-core';
import fs from 'fs';
import { PAGE, HAND } from './paths.mjs';
const URL_ = process.env.SITE || 'https://pingstats.aethercosmology.com/';
const d = JSON.parse(fs.readFileSync(HAND('cable_latency_survey_data_v2.json'), 'utf8'));
const b = await chromium.launch({ executablePath: process.env.CH });
const errs = [];
for (const [w, h] of [[1440, 900], [390, 844]]) {
const p = await b.newPage({ viewport: { width: w, height: h } });
p.on('pageerror', e => errs.push(`${w}: ${e.message}`)); p.on('console', m => { if (m.type() === 'error') errs.push(`${w}: ${m.text()}`) });
const res = await p.goto(URL_, { waitUntil: 'networkidle' }); await p.waitForTimeout(1500);
const r = await p.evaluate(() => ({ routes: LP.length, ae: [liveBad('ae'), liveVac('ae')], sw: document.documentElement.scrollWidth, fonts: [...document.fonts].filter(f => f.status === 'loaded').map(f => f.family).filter((x, i, a) => a.indexOf(x) === i) }));
const f = d.summary.all_live.cable_route.ae;
const out = {};
for (const [src, key] of [['ours', 'vultr'], ['gp', 'globalping'], ['home', 'home']]) { await p.selectOption('#srcsel', src); await p.waitForTimeout(150);
out[src] = await p.evaluate(() => [LP.length, liveBad('ae'), liveVac('ae')]);
const s = d.summary[key]; if (out[src][0] !== s.routes || out[src][1] !== s.cable_route.ae.impossible_total || out[src][2] !== s.cable_route.ae.beats_vacuum) errs.push(`${w} ${src} mismatch ${out[src]}`) }
await p.selectOption('#srcsel', 'all'); await p.waitForTimeout(400);
await p.screenshot({ path: `out/live_${w}.png` });
console.log(w, res.status(), JSON.stringify(r), JSON.stringify(out), r.routes === d.summary.all_live.routes && r.ae[0] === f.impossible_total && r.ae[1] === f.beats_vacuum && r.sw === w ? 'ok' : 'MISMATCH');
await p.close();
}
console.log('ERRORS', JSON.stringify(errs)); await b.close();
+37
View File
@@ -0,0 +1,37 @@
import { chromium } from 'playwright-core';
import fs from 'fs';
import { PAGE, HAND } from './paths.mjs';
const d = JSON.parse(fs.readFileSync(HAND('cable_latency_survey_data.json'), 'utf8'));
const b = await chromium.launch({ executablePath: process.env.CH });
const p = await b.newPage({ viewport: { width: 1440, height: 900 } });
await p.goto(PAGE('web/index.html'), { waitUntil: 'load' }); await p.waitForTimeout(2000);
const page = await p.evaluate(() => {
const out = {}, M = [["globe","globe"],["ae","ae"],["dp","dualpole"],["merc","mercator"]];
for (const [src, key] of [["gp","globalping"],["ours","vultr"],["home","home"],["all","all_live"]]) {
out[key] = { routes: LPS[src].length };
for (const [mode, kind] of [["cable","cable_route"],["line","straight"]]) { out[key][kind] = {};
for (const [m, mk] of M) { const L = LPS[src]; out[key][kind][mk] = [L.filter(q => q.r[mode][m].bad).length, L.filter(q => q.r[mode][m].vac).length]; } }
}
out.wondernetwork = { routes: WN.n }; for (const [mode, kind] of [["cable","cable_route"],["line","straight"]]) { out.wondernetwork[kind] = {}; for (const [m, mk] of M) out.wondernetwork[kind][mk] = [wnBad(m, mode), wnVac(m, mode)]; }
out.crossings = Object.fromEntries(M.map(([m, mk]) => [mk, [xBad(m), xVac(m)]]));
out.cables = Object.fromEntries(M.map(([m, mk]) => [mk, CAB.filter(c => cabBad(c, m)).length]));
return out;
});
let diffs = [];
for (const g of ["globalping", "vultr", "home", "all_live", "wondernetwork"]) {
const f = d.summary[g];
if (g !== "wondernetwork" && f.routes !== page[g].routes) diffs.push(`${g} routes file ${f.routes} page ${page[g].routes}`);
for (const kind of ["cable_route", "straight"]) for (const m of ["globe","ae","dualpole","mercator"]) {
const fv = [f[kind][m].impossible_total, f[kind][m].beats_vacuum], pv = page[g][kind][m];
if (fv[0] !== pv[0] || fv[1] !== pv[1]) diffs.push(`${g} ${kind} ${m}: file ${fv} page ${pv}`);
}
}
for (const m of ["globe","ae","dualpole","mercator"]) {
const fc = d.cable_crossings.filter(x => x.verdict_along_drawn_route).map(x => x.verdict_along_drawn_route[m]);
const fv = [fc.filter(v => v !== "ok").length, fc.filter(v => v === "vacuum").length];
if (fv[0] !== page.crossings[m][0] || fv[1] !== page.crossings[m][1]) diffs.push(`crossings ${m}: file ${fv} page ${page.crossings[m]}`);
const cb = d.cables.list.filter(c => c.too_short_for_map[m]).length;
if (cb !== page.cables[m]) diffs.push(`cables ${m}: file ${cb} page ${page.cables[m]}`);
}
console.log(diffs.length ? 'DIFFERENCES\n' + diffs.join('\n') : 'file and page agree on every count');
await b.close();
+20
View File
@@ -0,0 +1,20 @@
// Toggle Cable routes / Straight lines on every map and record what the page shows.
import { chromium } from 'playwright-core';
import { PAGE, HAND } from './paths.mjs';
const b = await chromium.launch({ executablePath: process.env.CH });
const p = await b.newPage({ viewport: { width: 1440, height: 900 } });
await p.goto(PAGE('site/index.html')); await p.waitForTimeout(1500);
for (const v of ['ae', 'dp', 'merc']) {
await p.click(`.models button[data-v="${v}"]`); await p.waitForTimeout(1200);
for (const mode of ['cable', 'line']) {
await p.click(`#mode button[data-mode="${mode}"]`); await p.waitForTimeout(900);
const r = await p.evaluate(() => ({ big: document.getElementById('rbig').textContent, lab: document.getElementById('rlab').innerText.replace(/\s+/g, ' ').slice(0, 120),
card: [...document.querySelectorAll('#card .row .top')].map(e => e.innerText.replace(/\s+/g, ' ')).join(' | '),
score: [...document.querySelectorAll('#score tbody tr')].map(r => r.cells[1].innerText.replace(/\s+/g, ' ')).join(' / '),
tab: document.querySelector('#card .tabs button[data-t="list"]').textContent }));
console.log(v, mode, JSON.stringify(r));
}
await p.click(`#mode button[data-mode="cable"]`);
}
await p.click('#mode button[data-mode="line"]'); await p.waitForTimeout(1500); await p.screenshot({ path: 'out/mode_line.png' });
await b.close();
+30
View File
@@ -0,0 +1,30 @@
// New sections: no errors; flat panels at zero correction equal the page's straight-line counts for every source;
// the last slider stop leaves nothing inconsistent; screenshots of each section.
import { chromium } from 'playwright-core';
import { PAGE, HAND } from './paths.mjs';
const b = await chromium.launch({ executablePath: process.env.CH });
const errs = [], fails = [];
for (const [w, h] of [[1440, 900], [390, 844]]) {
const p = await b.newPage({ viewport: { width: w, height: h } });
p.on('pageerror', e => errs.push(`${w}: ${e.message}`)); p.on('console', m => { if (m.type() === 'error' && !/net::|ERR_/.test(m.text())) errs.push(`${w}: ${m.text()}`) });
await p.goto(PAGE('site/index.html')); await p.waitForTimeout(1500);
for (const src of ['all', 'ours', 'gp', 'home']) {
await p.selectOption('#srcsel', src); await p.waitForTimeout(150);
const r = await p.evaluate(() => { const out = {};
for (const [m, , k] of FFM) { const f = D.flat.fits[S.src][k]; out[m] = [f.budgets[0].inconsistent.length, liveBad(m, 'line'), f.budgets[f.budgets.length - 1].inconsistent.length, f.routes, LP.length] }
return out });
for (const [m, [b0, page, last, nr, lp]] of Object.entries(r)) if (b0 !== page || last !== 0 || nr !== lp) fails.push(`${w} ${src} ${m}: fit ${b0} page ${page} last ${last} routes ${nr}/${lp}`);
}
await p.selectOption('#srcsel', 'all'); await p.waitForTimeout(200);
for (const [sec, name] of [['Networks on the measured paths', 'net'], ['Best attainable flat arrangements', 'ff'], ['Code and data', 'code']]) {
const el = p.locator('h2.sec', { hasText: sec }).locator('xpath=..');
if (name === 'ff') for (const v of [0, 4, 6]) { await p.fill('#ffb', String(v)); await p.dispatchEvent('#ffb', 'input'); await p.waitForTimeout(250); await el.screenshot({ path: `out/sec_${name}${v}_${w}.png` }) }
else await el.screenshot({ path: `out/sec_${name}_${w}.png` });
}
const sw = await p.evaluate(() => document.documentElement.scrollWidth);
if (sw !== w) fails.push(`${w}: page scrollWidth ${sw}`);
const links = await p.evaluate(() => [...document.querySelectorAll('main a')].map(a => a.href));
console.log(w, 'links', links.length, [...new Set(links.map(u => new URL(u).host))]);
await p.close();
}
console.log('FAILS', JSON.stringify(fails)); console.log('ERRORS', JSON.stringify(errs)); await b.close();
+21
View File
@@ -0,0 +1,21 @@
import { chromium } from 'playwright-core';
import fs from 'fs';
import { PAGE, HAND } from './paths.mjs';
const d = JSON.parse(fs.readFileSync(HAND('cable_latency_survey_data.json'), 'utf8'));
const b = await chromium.launch({ executablePath: process.env.CH });
const errs = [], blocked = [];
for (const [w, h] of [[1440, 900], [390, 844]]) {
const p = await b.newPage({ viewport: { width: w, height: h } });
await p.route('**/*', r => { const u = r.request().url(); if (u.startsWith('file:')) return r.continue(); blocked.push(u); return r.abort(); });
p.on('pageerror', e => errs.push(`${w}: ${e.message}`)); p.on('console', m => { if (m.type() === 'error' && !/ERR_FAILED|net::/.test(m.text())) errs.push(`${w}: ${m.text()}`) });
await p.goto(PAGE('site/index.html'), { waitUntil: 'load' }); await p.waitForTimeout(2500);
const r = await p.evaluate(() => ({ d3: typeof d3, drawn: hits.length, routes: LP.length, ae: [liveBad('ae'), liveVac('ae')], sw: document.documentElement.scrollWidth, doctype: document.doctype && document.doctype.name, title: document.title }));
const f = d.summary.all_live.cable_route.ae;
console.log(w, JSON.stringify(r), r.routes === d.summary.all_live.routes && r.ae[0] === f.impossible_total && r.ae[1] === f.beats_vacuum ? 'matches data file' : 'MISMATCH');
for (const src of ['ours', 'gp', 'home']) { await p.selectOption('#srcsel', src); await p.waitForTimeout(200); }
await p.selectOption('#srcsel', 'all'); await p.waitForTimeout(300);
await p.screenshot({ path: `out/offline_${w}.png` });
await p.close();
}
console.log('blocked external requests:', [...new Set(blocked.map(u => new URL(u).host))]);
console.log('ERRORS', JSON.stringify(errs)); await b.close();
+6
View File
@@ -0,0 +1,6 @@
{
"private": true,
"dependencies": {
"playwright-core": "1.49.1"
}
}
+6
View File
@@ -0,0 +1,6 @@
// Repository-relative paths, so the checks run from the quest tree and from the public repository alike.
import { existsSync } from 'fs';
export const ROOT = new URL('../', import.meta.url).pathname;
export const PAGE = (p) => 'file://' + ROOT + 'src/' + p;
// the hand-off files sit in src/handoff/ in the quest tree and at the repository root in the public copy
export const HAND = (f) => existsSync(ROOT + 'src/handoff/' + f) ? ROOT + 'src/handoff/' + f : ROOT + f;
Executable
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# Browser checks for src/web/index.html and src/site/index.html.
# Needs: npm install (playwright-core) and a Chromium binary in $CH.
set -u
cd "$(dirname "$0")" && mkdir -p out
: "${CH:?set CH to a Chromium executable, e.g. ~/.cache/ms-playwright/chromium-*/chrome-linux64/chrome}"
export CH
node match.mjs # page counts = hand-off data file
node src.mjs | grep FAILS # each data source shows only its own data
node audit.mjs 2>&1 | grep -v '^INFO' # 8 viewports: clickability, overlap, overflow
node fit.mjs | grep -v ' 0px$' || true # route panel fits the hero
node card_sel.mjs # every route x map x size: no card overflow
node trunc.mjs | tail -1 # no truncated text in the route card
node view_btns.mjs | tail -2 # zoom / reset buttons
node offline.mjs | tail -3 # site/index.html with the network blocked
node newsec.mjs | tail -2 # networks, flat arrangements, code: counts and layout
node mode.mjs | cut -c1-60 # cable / straight toggle changes the counts
[ "${LIVE:-0}" = 1 ] && node live.mjs # LIVE=1: the public site
+22
View File
@@ -0,0 +1,22 @@
import { chromium } from 'playwright-core';
import { PAGE, HAND } from './paths.mjs';
const b = await chromium.launch({ executablePath: process.env.CH });
const p = await b.newPage({ viewport: { width: 1440, height: 900 } });
const errs = []; p.on('pageerror', e => errs.push(e.message)); p.on('console', m => { if (m.type() === 'error') errs.push(m.text()) });
await p.goto(PAGE('web/index.html'), { waitUntil: 'load' }); await p.waitForTimeout(2500);
const fails = [];
for (const src of ['ours', 'gp', 'home', 'all']) {
await p.selectOption('#srcsel', src); await p.waitForTimeout(1000);
const r = await p.evaluate(() => ({ n: LP.length, ae: [liveBad('ae'), liveVac('ae')], big: document.getElementById('rbig').textContent, lab: document.getElementById('rlab').innerText.replace(/\s+/g,' '),
sel: `${LS[LP[S.sel].a].name}-${LS[LP[S.sel].b].name}`, by: document.querySelector('#card .meas span')?.innerText,
score: document.querySelector('#score tbody tr:nth-child(2) td:nth-child(2)').innerText.replace(/\s+/g,' '),
vultrOnly: LP.every(q => q.cells.every(c => /Vultr/.test(c.net))), gpOnly: LP.every(q => q.cells.every(c => !/Vultr|^home/.test(c.net))), homeOnly: LP.every(q => q.cells.every(c => /^home/.test(c.net))), drawn: hits.length }));
await p.waitForTimeout(800);
if (+r.big !== r.ae[0]) fails.push(`${src}: counter ${r.big} != ${r.ae[0]}`);
if (src === 'ours' && !r.vultrOnly) fails.push('ours: non-Vultr cells present');
if (src === 'gp' && !r.gpOnly) fails.push('gp: other sources present');
if (src === 'home' && !r.homeOnly) fails.push('home: other sources present');
console.log(src, JSON.stringify(r));
await p.screenshot({ path: `out/src_${src}.png` });
}
console.log('FAILS', JSON.stringify(fails), 'ERRORS', JSON.stringify(errs)); await b.close();
+21
View File
@@ -0,0 +1,21 @@
import { chromium } from 'playwright-core';
import { PAGE, HAND } from './paths.mjs';
const b = await chromium.launch({ executablePath: process.env.CH });
const bad = [];
for (const [w, h] of [[1920, 1080], [1440, 900], [1366, 768], [1280, 800], [1024, 768], [390, 844]]) {
const p = await b.newPage({ viewport: { width: w, height: h } });
await p.goto(PAGE('web/index.html')); await p.waitForTimeout(1200);
const r = await p.evaluate(async () => {
const out = [];
for (const v of ['globe', 'ae', 'dp', 'merc']) { document.querySelector(`.models button[data-v="${v}"]`).click();
for (const src of ['all', 'ours', 'gp', 'home']) { setSource(src);
for (let i = 0; i < LP.length; i++) { select(i);
const t = [...document.querySelectorAll('#card .row .top b, #card .row .top em, #card h2')].filter(e => e.scrollWidth > e.clientWidth + 1);
if (t.length) out.push(`${v} ${src} ${LS[LP[i].a].name}-${LS[LP[i].b].name}: ${t.map(e => e.textContent).join(' | ')}`) } } }
return { out, nokm: document.getElementById('card').classList.contains('nokm') };
});
console.log(w, h, 'truncated rows:', r.out.length, r.out.slice(0, 2));
if (r.out.length) bad.push(w);
await p.close();
}
console.log('TRUNC FAILS', bad); await b.close();
+38
View File
@@ -0,0 +1,38 @@
import { chromium } from 'playwright-core';
import { PAGE, HAND } from './paths.mjs';
const b = await chromium.launch({ executablePath: process.env.CH });
const errs = [], fails = [];
const p = await b.newPage({ viewport: { width: 1440, height: 900 } });
p.on('pageerror', e => errs.push(e.message)); p.on('console', m => { if (m.type() === 'error') errs.push(m.text()) });
await p.goto(PAGE('web/index.html'), { waitUntil: 'load' }); await p.waitForTimeout(2500);
const st = () => p.evaluate(() => ({ view: S.view, k: T[S.view].k, x: T[S.view].x, y: T[S.view].y, rot: globeRot.slice(), fit: FIT[S.view], dis: document.getElementById('treset').disabled }));
const near = (a, b) => Math.abs(a.k / b.k - 1) < .003 && Math.abs(a.x - b.x) < 1.5 && Math.abs(a.y - b.y) < 1.5;
for (const v of ['ae', 'globe', 'dp', 'merc']) {
if (v !== 'ae') { await p.click(`.models button[data-v="${v}"]`); await p.waitForTimeout(1300); }
let s0 = await st();
if (!s0.dis) fails.push(`${v}: Reset enabled on a fresh view`);
await p.click('#tzoom'); await p.waitForTimeout(900);
let s1 = await st();
const changed = !near(s1, s0) || (v === 'globe' && Math.hypot(s1.rot[0] - s0.rot[0], s1.rot[1] - s0.rot[1]) > 1);
if (!changed) fails.push(`${v}: Zoom to route changed nothing`);
if (s1.dis) fails.push(`${v}: Reset still disabled after zooming`);
await p.screenshot({ path: `out/zoom_${v}.png` });
await p.click('#treset'); await p.waitForTimeout(900);
let s2 = await st();
if (!near(s2, s2.fit)) fails.push(`${v}: Reset did not return to the fitted view ${JSON.stringify([s2.k, s2.fit.k])}`);
if (v === 'globe' && Math.hypot(s2.rot[0] + 160, s2.rot[1] - 25) > .5) fails.push(`globe: Reset left rotation at ${s2.rot}`);
if (!s2.dis) fails.push(`${v}: Reset not disabled after resetting`);
console.log(`${v}: zoom k ${s0.k.toFixed(1)} -> ${s1.k.toFixed(1)}, rot ${s0.rot.map(n=>n.toFixed(0))} -> ${s1.rot.map(n=>n.toFixed(0))}; reset -> k ${s2.k.toFixed(1)} rot ${s2.rot.map(n=>n.toFixed(0))}`);
}
// globe: drag, then reset must undo the rotation
await p.click('.models button[data-v="globe"]'); await p.waitForTimeout(1300);
await p.mouse.move(600, 400); await p.mouse.down(); await p.mouse.move(800, 300, { steps: 8 }); await p.mouse.up(); await p.waitForTimeout(100);
let d = await st(); if (d.dis) fails.push('globe: Reset disabled after dragging');
await p.click('#treset'); await p.waitForTimeout(900); d = await st();
if (Math.hypot(d.rot[0] + 160, d.rot[1] - 25) > .5) fails.push(`globe: Reset after drag left rotation ${d.rot}`);
// another route, zoomed on AE
await p.click('.models button[data-v="ae"]'); await p.waitForTimeout(1300);
await p.evaluate(() => { const a = LS.findIndex(s => s.name === 'Fortaleza'), c = LS.findIndex(s => s.name === 'Luanda'); select(LP.find(q => (q.a === a && q.b === c) || (q.a === c && q.b === a)).idx); });
await p.click('#tzoom'); await p.waitForTimeout(900); await p.screenshot({ path: 'out/zoom_ae_sacs.png' });
console.log('FAILS', JSON.stringify(fails)); console.log('ERRORS', JSON.stringify(errs));
await b.close();