Files
meshcore-analyzer/tests/e2e/test-node-reach-e2e.js
T
Alex B 893773338e chore(tests): move root test-*.js into tests/unit and tests/e2e (#2036)
Moves 290 root test-*.js into tests/unit (177, listed in test-all.sh) and tests/e2e (113, classified in scripts/non-unit-tests.json), per #1981 and PR-D of #1385. Root goes from 348 entries to 48. test-all.sh and test-fixtures/ stay put. The inventory guard now fails if a test reappears in the root or sits in the wrong folder.

Verified independently of the diff: the invoked sets are unchanged (test-all.sh 177 before and after, deploy.yml 96 before and after, both identical as sets), and a full local run of test-all.sh on master and on the branch produced 4702 output lines each whose only differences are absolute paths, stack-trace line numbers shifted by the REPO_ROOT line, the inventory wording and two perf ratios. The guard was mutation-checked: a test back in the root, a unit suite in tests/e2e, and a suite dropped from test-all.sh each make it exit 1. CI run 35246304316 ran 97 suites from tests/e2e and is green.

Follow-up 9335c51d finished the instruction files: no bare root test command is left in AGENTS.md, the squad charters, .github or docs, and every tests/ path they name resolves.

Merged by the interim maintainer without a second human reviewer: CI and the local runs above are the independent checks.

Known and deliberately out of scope: 18 of the 113 files in tests/e2e are invoked by no runner at all, and one of them cannot run anywhere because it requires jsdom, which is not a declared dependency. Tracked separately.
2026-09-17 19:06:07 +02:00

77 lines
3.4 KiB
JavaScript

// E2E for the per-node Reach page (#/nodes/<pubkey>/reach).
// Defaults to localhost:3000 — NEVER point at prod (AGENTS.md). CI sets BASE_URL.
const { chromium } = require('playwright');
const BASE = process.env.BASE_URL || 'http://localhost:3000';
async function getJson(page, url) {
const resp = await page.request.get(url);
if (!resp.ok()) throw new Error('GET ' + url + ' → HTTP ' + resp.status());
return resp.json();
}
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
// A repeater is most likely to have reach data (it relays).
const nodes = await getJson(page, BASE + '/api/nodes?role=repeater&limit=1');
if (!nodes.nodes || !nodes.nodes.length) {
console.log('node-reach E2E SKIP (no repeater in dataset)');
await browser.close();
return;
}
const pk = nodes.nodes[0].public_key;
// 1. The endpoint returns the documented shape.
const reach = await getJson(page, BASE + '/api/nodes/' + pk + '/reach?days=7');
for (const k of ['node', 'window', 'reliable_tokens', 'importance', 'links', 'direct_observers']) {
if (!(k in reach)) throw new Error('reach response missing key: ' + k);
}
if (!Array.isArray(reach.links)) throw new Error('reach.links must be an array');
// 2. The page renders.
await page.goto(BASE + '/#/nodes/' + pk + '/reach');
await page.waitForSelector('.nq-head', { timeout: 20000 });
if (!(await page.locator('h2', { hasText: 'Reach' }).count())) {
throw new Error('Reach header missing');
}
// 3. If this node is identifiable, exercise the table, toggles and links.
if (reach.reliable_tokens.length && (await page.locator('#nqRows').count())) {
await page.waitForSelector('#nqIncoming');
await page.waitForSelector('#nqOutgoing');
// Derive the EXACT expected row counts from the API so the toggles are
// verified, not just "didn't shrink" (tautology). Base shows two-way only;
// incoming adds we-only links; +outgoing adds the rest (= all links).
const twoWayExp = reach.links.filter(l => l.bidir).length;
const weOnlyExp = reach.links.filter(l => !l.bidir && l.we_hear > 0 && l.they_hear === 0).length;
const allExp = reach.links.length;
const base = await page.locator('#nqRows tr').count();
if (base !== twoWayExp) throw new Error(`base rows ${base} != two-way ${twoWayExp}`);
await page.check('#nqIncoming');
const withIncoming = await page.locator('#nqRows tr').count();
if (withIncoming !== twoWayExp + weOnlyExp) {
throw new Error(`incoming rows ${withIncoming} != two-way+we-only ${twoWayExp + weOnlyExp}`);
}
await page.check('#nqOutgoing');
const withBoth = await page.locator('#nqRows tr').count();
if (withBoth !== allExp) throw new Error(`both-toggles rows ${withBoth} != all links ${allExp}`);
// Neighbour rows link to a node detail page.
if (await page.locator('#nqRows a.nq-link').count()) {
const href = await page.locator('#nqRows a.nq-link').first().getAttribute('href');
if (!href || !href.startsWith('#/nodes/')) throw new Error('neighbour link malformed: ' + href);
}
// Map must render whenever at least one link has GPS (no swallowed failure).
if (reach.links.some(l => l.lat != null && l.lon != null)) {
await page.waitForSelector('#nqMap .leaflet-container', { timeout: 10000 });
}
}
console.log('node-reach E2E OK');
await browser.close();
})().catch((e) => { console.error(e); process.exit(1); });