Files
meshcore-analyzer/tests/e2e/test-live-dedup.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

180 lines
6.7 KiB
JavaScript

/**
* Tests for Live page hash-based packet deduplication in the feed.
* Injects packets by intercepting WebSocket before page loads.
*
* Usage:
* CHROMIUM_PATH=/usr/bin/chromium-browser BASE_URL=http://localhost:13581 node test-live-dedup.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 run() {
console.log('Launching Chromium for Live dedup tests...');
const browser = await chromium.launch({
headless: true,
executablePath: process.env.CHROMIUM_PATH || undefined,
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage']
});
const context = await browser.newContext();
// Patch WebSocket BEFORE any page script runs
await context.addInitScript(() => {
const OrigWS = window.WebSocket;
window.__capturedWS = [];
window.WebSocket = function(...args) {
const ws = new OrigWS(...args);
window.__capturedWS.push(ws);
return ws;
};
window.WebSocket.prototype = OrigWS.prototype;
window.WebSocket.CONNECTING = OrigWS.CONNECTING;
window.WebSocket.OPEN = OrigWS.OPEN;
window.WebSocket.CLOSING = OrigWS.CLOSING;
window.WebSocket.CLOSED = OrigWS.CLOSED;
});
const page = await context.newPage();
page.setDefaultTimeout(15000);
console.log(`\nRunning Live dedup tests against ${BASE}\n`);
// Helper: navigate to live page, wait for feed, clear initial items
async function setupLivePage() {
await page.goto(`${BASE}/#/live`, { waitUntil: 'networkidle' });
await page.waitForSelector('#liveFeed', { timeout: 10000 });
await page.waitForTimeout(3000); // let WS connect + initial replay
// Clear feed
await page.evaluate(() => {
document.getElementById('liveFeed').querySelectorAll('.live-feed-item').forEach(el => el.remove());
});
}
// Helper: inject a packet via captured WS
function injectPkt(hash, observer, type, text, id) {
return page.evaluate(({hash, observer, type, text, id}) => {
// Find the last active WebSocket with an onmessage handler
const wsList = window.__capturedWS || [];
let ws = null;
for (let i = wsList.length - 1; i >= 0; i--) {
if (wsList[i].onmessage && wsList[i].readyState === 1) { ws = wsList[i]; break; }
}
if (!ws) throw new Error('No active WebSocket found (count: ' + wsList.length + ')');
ws.onmessage({ data: JSON.stringify({ type: 'packet', data: {
id: id || 'test-' + Math.random().toString(36).slice(2),
hash: hash || undefined,
raw: 'AABB' + (hash || '0000').slice(0, 4),
decoded: {
header: { payloadTypeName: type || 'GRP_TXT' },
payload: { text: text || 'test msg' },
path: { hops: ['ab', 'cd'] }
},
snr: 10, rssi: -85, observer_name: observer || 'obs1'
}})});
}, {hash, observer, type, text, id});
}
await setupLivePage();
await test('Duplicate hash packets produce single feed entry', async () => {
const HASH = 'aabbccdd11223344';
await injectPkt(HASH, 'observer-A', 'GRP_TXT', 'hello');
await injectPkt(HASH, 'observer-B', 'GRP_TXT', 'hello');
await page.waitForTimeout(300);
const items = await page.$$eval(`.live-feed-item[data-hash="${HASH}"]`, els => els.length);
assert(items === 1, `Expected 1 feed item for hash, got ${items}`);
// Check observation badge shows 2
const badgeText = await page.$eval(`.live-feed-item[data-hash="${HASH}"] .badge-obs`, el => el.textContent);
assert(badgeText.includes('2'), `Badge should show 2, got "${badgeText}"`);
});
// Clear feed between tests
await page.evaluate(() => {
document.getElementById('liveFeed').querySelectorAll('.live-feed-item').forEach(el => el.remove());
});
await test('Different hash packets produce separate feed entries', async () => {
await injectPkt('bbbb111122223333', 'obs1', 'ADVERT', '', 'b1');
await injectPkt('cccc444455556666', 'obs1', 'TXT_MSG', 'direct', 'c1');
await page.waitForTimeout(300);
const count = await page.$$eval('.live-feed-item', els => els.length);
assert(count === 2, `Expected 2 items, got ${count}`);
});
await page.evaluate(() => {
document.getElementById('liveFeed').querySelectorAll('.live-feed-item').forEach(el => el.remove());
});
await test('Rapid sequential duplicates (5 observers) aggregate correctly', async () => {
const HASH = 'dddddddd33333333';
for (let i = 0; i < 5; i++) {
await injectPkt(HASH, 'obs-' + i, 'GRP_TXT', 'flood', 'td-' + i);
}
await page.waitForTimeout(300);
const items = await page.$$eval(`.live-feed-item[data-hash="${HASH}"]`, els => els.length);
assert(items === 1, `Expected 1 feed item for 5 observations, got ${items}`);
const badgeText = await page.$eval(`.live-feed-item[data-hash="${HASH}"] .badge-obs`, el => el.textContent);
assert(badgeText.includes('5'), `Badge should show 5, got "${badgeText}"`);
});
await page.evaluate(() => {
document.getElementById('liveFeed').querySelectorAll('.live-feed-item').forEach(el => el.remove());
});
await test('Same hash same observer still deduplicates', async () => {
const HASH = 'eeeeeeee44444444';
await injectPkt(HASH, 'same-obs', 'GRP_TXT', 'dup', 'e1');
await injectPkt(HASH, 'same-obs', 'GRP_TXT', 'dup', 'e2');
await page.waitForTimeout(300);
const count = await page.$$eval(`.live-feed-item[data-hash="${HASH}"]`, els => els.length);
assert(count === 1, `Expected 1 feed item, got ${count}`);
});
await page.evaluate(() => {
document.getElementById('liveFeed').querySelectorAll('.live-feed-item').forEach(el => el.remove());
});
await test('Packets without hash are not deduplicated', async () => {
await injectPkt(null, 'obs1', 'ACK', '', 'nh1');
await injectPkt(null, 'obs2', 'ACK', '', 'nh2');
await page.waitForTimeout(300);
const count = await page.$$eval('.live-feed-item', els => els.length);
assert(count === 2, `Expected 2 items for no-hash packets, got ${count}`);
});
await browser.close();
const passed = results.filter(r => r.pass).length;
const failed = results.filter(r => !r.pass).length;
console.log(`\n${passed}/${results.length} tests passed${failed ? `, ${failed} failed` : ''}`);
process.exit(failed > 0 ? 1 : 0);
}
run().catch(err => {
console.error('Fatal error:', err);
process.exit(1);
});