Files
meshcore-analyzer/tests/e2e/test-nodes-export-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

93 lines
3.7 KiB
JavaScript

// E2E for the Nodes JSON export button (#/nodes → "Export JSON").
// 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';
const REF_KEYS = ['type', 'name', 'custom_name', 'public_key', 'flags', 'latitude',
'longitude', 'last_advert', 'last_modified', 'out_path_list'];
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage({ acceptDownloads: true });
await page.goto(BASE + '/#/nodes');
await page.waitForSelector('#nodesLeft[data-loaded="true"]', { timeout: 30000 });
const btn = await page.$('#nodesExportBtn');
if (!btn) throw new Error('#nodesExportBtn is missing from the nodes topbar');
const label = (await btn.textContent()).trim();
if (await btn.isDisabled()) {
// No exportable node in this dataset (all lack a name or a position).
if (label !== 'Export JSON') {
throw new Error('disabled button should carry no count, got "' + label + '"');
}
console.log('nodes-export E2E SKIP (no exportable node in dataset)');
await browser.close();
return;
}
const m = label.match(/^Export JSON \((\d+)\)$/);
if (!m) throw new Error('enabled button must show the contact count, got "' + label + '"');
const expectedCount = Number(m[1]);
const [download] = await Promise.all([
page.waitForEvent('download', { timeout: 15000 }),
btn.click(),
]);
const name = download.suggestedFilename();
if (!/^corescope_nodes_[A-Za-z0-9_-]+_\d{4}-\d{2}-\d{2}-\d{6}\.json$/.test(name)) {
throw new Error('unexpected download filename: ' + name);
}
const stream = await download.createReadStream();
let raw = '';
for await (const chunk of stream) raw += chunk;
const payload = JSON.parse(raw);
if (!Array.isArray(payload.contacts)) throw new Error('export must have a contacts array');
if (payload.contacts.length !== expectedCount) {
throw new Error('button said ' + expectedCount + ' contacts, file has ' + payload.contacts.length);
}
if (Object.keys(payload).length !== 1) {
throw new Error('export must contain only the contacts key, got ' + Object.keys(payload).join(','));
}
payload.contacts.forEach(function (c, i) {
const keys = Object.keys(c).join(',');
if (keys !== REF_KEYS.join(',')) {
throw new Error('contact ' + i + ' key mismatch: ' + keys);
}
if (typeof c.public_key !== 'string' || c.public_key.length < 64) {
throw new Error('contact ' + i + ' has a truncated public_key');
}
if (typeof c.latitude !== 'string' || typeof c.longitude !== 'string') {
throw new Error('contact ' + i + ' coordinates must be strings');
}
if (c.latitude === '0' && c.longitude === '0') {
throw new Error('contact ' + i + ' is at null island and should have been skipped');
}
if (typeof c.last_advert !== 'number' || typeof c.type !== 'number') {
throw new Error('contact ' + i + ' last_advert/type must be numbers');
}
});
// The export is WYSIWYG: narrowing the table with the search box must narrow
// the export too.
const first = payload.contacts[0].name;
await page.fill('#nodeSearch', first);
await page.waitForFunction(function (want) {
var b = document.getElementById('nodesExportBtn');
return b && b.textContent.trim() !== want;
}, label, { timeout: 15000 });
const narrowed = (await page.textContent('#nodesExportBtn')).trim();
const nm = narrowed.match(/^Export JSON \((\d+)\)$/);
if (!nm || Number(nm[1]) >= expectedCount) {
throw new Error('search should shrink the export set, got "' + narrowed + '" vs ' + expectedCount);
}
console.log('nodes-export E2E OK (' + expectedCount + ' contacts, ' + name + ')');
await browser.close();
})();