Files
meshcore-analyzer/tests/e2e/test-packets-scope-column.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

154 lines
6.6 KiB
JavaScript

/**
* Playwright E2E — Scope column on the Packets tab.
*
* transmissions.scope_name has three states and the column must render each one
* distinguishably: em dash (not transport-scoped), muted "unknown"
* (transport-scoped, region unmatched) and the region name on a match.
*
* Usage: node test-packets-scope-column.js
* BASE_URL=https://staging.on8ar.eu node test-packets-scope-column.js
*/
const { chromium } = require('playwright');
const BASE = process.env.BASE_URL || 'http://localhost:3000';
const results = [];
async function test(name, fn) {
try {
await fn();
results.push({ name, pass: true });
console.log(` ✅ ${name}`);
} catch (err) {
results.push({ name, pass: false, error: err.message });
console.log(` ❌ ${name}: ${err.message}`);
}
}
function assert(condition, msg) {
if (!condition) throw new Error(msg || 'Assertion failed');
}
async function gotoPackets(page) {
await page.goto(BASE + '/#/packets', { waitUntil: 'domcontentloaded' });
await page.evaluate(() => {
localStorage.removeItem('packets-visible-cols');
localStorage.removeItem('packets-known-cols');
});
await page.reload({ waitUntil: 'networkidle' });
await page.waitForSelector('#pktTable tbody tr:not([id^=vscroll])', { timeout: 30000 });
}
(async () => {
console.log(`\nPackets Scope column — ${BASE}\n`);
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1400, height: 1000 } });
await gotoPackets(page);
await test('Scope header sits between Type and Observer', async () => {
const headers = await page.$$eval('#pktTable thead th', ths =>
ths.map(th => th.textContent.trim()));
const iType = headers.indexOf('Type');
const iScope = headers.indexOf('Scope');
const iObserver = headers.indexOf('Observer');
assert(iScope !== -1, 'Scope header missing, got: ' + headers.join('|'));
assert(iType < iScope && iScope < iObserver,
`expected Type < Scope < Observer, got ${iType} < ${iScope} < ${iObserver}`);
});
await test('Scope column is visible by default', async () => {
const hidden = await page.$eval('#pktTable', t => t.classList.contains('hide-col-scope'));
assert(!hidden, 'table carries hide-col-scope on a fresh visit');
const cellCount = await page.$$eval('#pktTable tbody td.col-scope', tds => tds.length);
assert(cellCount > 0, 'no td.col-scope rendered');
});
await test('every row renders exactly one scope cell', async () => {
const { rows, cells } = await page.evaluate(() => {
const trs = Array.from(document.querySelectorAll('#pktTable tbody tr'))
.filter(tr => !tr.id.startsWith('vscroll') && tr.querySelector('td.col-type'));
return {
rows: trs.length,
cells: trs.filter(tr => tr.querySelectorAll('td.col-scope').length === 1).length,
};
});
assert(rows > 0, 'no packet rows found');
assert(rows === cells, `${rows} rows but ${cells} have exactly one scope cell`);
});
await test('non-transport rows render an em dash', async () => {
const found = await page.evaluate(() => {
for (const tr of document.querySelectorAll('#pktTable tbody tr')) {
const type = tr.querySelector('td.col-type');
const scope = tr.querySelector('td.col-scope');
if (!type || !scope) continue;
// No T badge → FLOOD or DIRECT → no transport scope possible.
if (!type.querySelector('.badge-transport')) return scope.textContent.trim();
}
return null;
});
assert(found !== null, 'no non-transport row on screen to check');
assert(found === '—', `expected em dash, got "${found}"`);
});
await test('sorting by Scope pins the empties last', async () => {
await page.click('#pktTable thead th.col-scope');
await page.waitForTimeout(600);
const values = await page.$$eval('#pktTable tbody td.col-scope', tds =>
tds.map(td => td.textContent.trim()));
const lastScoped = values.reduce((acc, v, i) => (v !== '—' ? i : acc), -1);
const firstEmpty = values.indexOf('—');
if (lastScoped === -1 || firstEmpty === -1) {
console.log(' (only one scope state on screen, ordering not exercised)');
return;
}
assert(firstEmpty > lastScoped,
`em dashes must follow every scoped row; first dash at ${firstEmpty}, last scoped at ${lastScoped}`);
});
await test('Columns menu can hide and restore the Scope column', async () => {
// A checkbox click bubbles to the document handler that closes the menu, so
// each toggle needs its own open.
const toggleScope = async () => {
await page.click('#colToggleBtn');
await page.waitForTimeout(200);
const box = await page.$('#colToggleMenu input[data-col="scope"]');
assert(box, 'no Scope checkbox in the Columns menu');
const wasChecked = await box.isChecked();
await box.click();
await page.waitForTimeout(300);
return wasChecked;
};
assert(await toggleScope(), 'Scope checkbox should start checked');
assert(await page.$eval('#pktTable', t => t.classList.contains('hide-col-scope')),
'unchecking should add hide-col-scope');
assert(!(await toggleScope()), 'Scope checkbox should now be unchecked');
assert(!(await page.$eval('#pktTable', t => t.classList.contains('hide-col-scope'))),
're-checking should remove hide-col-scope');
});
await test('a column added after the visitor saved prefs arrives visible', async () => {
// Simulate a returning visitor whose stored prefs predate the Scope column.
await page.evaluate(() => {
localStorage.setItem('packets-visible-cols',
JSON.stringify(['time', 'hash', 'size', 'type', 'observer', 'path', 'rpt', 'details']));
localStorage.removeItem('packets-known-cols');
});
await page.reload({ waitUntil: 'networkidle' });
await page.waitForSelector('#pktTable tbody tr:not([id^=vscroll])', { timeout: 30000 });
assert(!(await page.$eval('#pktTable', t => t.classList.contains('hide-col-scope'))),
'Scope should be shown for prefs saved before the column existed');
// Region was explicitly absent from those prefs AND predates the column, so
// the backfill must not resurrect it — only genuinely new keys get defaulted.
assert(await page.$eval('#pktTable', t => t.classList.contains('hide-col-region')),
'backfill must not re-enable a column the visitor had hidden');
});
await browser.close();
const failed = results.filter(r => !r.pass);
console.log(`\n=== ${results.length - failed.length} passed, ${failed.length} failed ===`);
process.exit(failed.length ? 1 : 0);
})();