test: expanded frontend coverage collection with page interactions

367 lines of Playwright interactions covering nodes, packets, map,
analytics, customizer, channels, live, home pages.
Fixed e2e channels assertion (chList vs chResp.channels).
This commit is contained in:
you
2026-03-24 03:43:27 +00:00
parent c4c2565fa7
commit 72d06590c4
3 changed files with 333 additions and 11 deletions

View File

@@ -1,7 +1,8 @@
// After Playwright tests, this script:
// 1. Connects to the running test server
// 2. Extracts window.__coverage__ from the browser
// 3. Writes it to .nyc_output/ for merging
// 2. Exercises frontend interactions to maximize code coverage
// 3. Extracts window.__coverage__ from the browser
// 4. Writes it to .nyc_output/ for merging
const { chromium } = require('playwright');
const fs = require('fs');
@@ -10,25 +11,345 @@ const path = require('path');
async function collectCoverage() {
const browser = await chromium.launch({
executablePath: process.env.CHROMIUM_PATH || '/usr/bin/chromium',
args: ['--no-sandbox'],
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
headless: true
});
const page = await browser.newPage();
page.setDefaultTimeout(10000);
const BASE = process.env.BASE_URL || 'http://localhost:13581';
// Visit every major page to exercise the code
const pages = ['#/home', '#/nodes', '#/map', '#/packets', '#/channels', '#/analytics', '#/live', '#/traces', '#/observers'];
for (const hash of pages) {
await page.goto(`${BASE}/${hash}`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
// Helper: safe click
async function safeClick(selector, timeout) {
try {
await page.click(selector, { timeout: timeout || 3000 });
await page.waitForTimeout(400);
} catch {}
}
// Helper: safe fill
async function safeFill(selector, text) {
try {
await page.fill(selector, text);
await page.waitForTimeout(400);
} catch {}
}
// Helper: safe select
async function safeSelect(selector, value) {
try {
await page.selectOption(selector, value);
await page.waitForTimeout(400);
} catch {}
}
// ── HOME PAGE ──
console.log(' [coverage] Home page...');
await page.goto(`${BASE}/#/home`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
await page.waitForTimeout(1500);
// Click onboarding buttons
await safeClick('#chooseNew');
await page.waitForTimeout(800);
// Click FAQ items if present
const faqItems = await page.$$('.faq-q, .question, [class*="accordion"]').catch(() => []);
for (let i = 0; i < Math.min(faqItems.length, 3); i++) {
try { await faqItems[i].click(); await page.waitForTimeout(300); } catch {}
}
// Click cards
const cards = await page.$$('.card, .health-card, [class*="card"]').catch(() => []);
for (let i = 0; i < Math.min(cards.length, 3); i++) {
try { await cards[i].click(); await page.waitForTimeout(300); } catch {}
}
// Toggle level
await safeClick('#toggleLevel');
await page.waitForTimeout(500);
// Go back to home and choose experienced
await page.goto(`${BASE}/#/home`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
await page.waitForTimeout(1000);
await safeClick('#chooseExp');
await page.waitForTimeout(1000);
// Click journey timeline items
const timelineItems = await page.$$('.timeline-item, [class*="journey"]').catch(() => []);
for (let i = 0; i < Math.min(timelineItems.length, 5); i++) {
try { await timelineItems[i].click(); await page.waitForTimeout(300); } catch {}
}
// ── NODES PAGE ──
console.log(' [coverage] Nodes page...');
await page.goto(`${BASE}/#/nodes`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
await page.waitForTimeout(2000);
// Click column headers to sort
const headers = await page.$$('th');
for (let i = 0; i < Math.min(headers.length, 6); i++) {
try { await headers[i].click(); await page.waitForTimeout(300); } catch {}
}
// Click first header again for reverse sort
if (headers.length > 0) try { await headers[0].click(); await page.waitForTimeout(300); } catch {}
// Click role tabs using data-tab attribute
const roleTabs = await page.$$('.node-tab[data-tab]');
for (const tab of roleTabs) {
try { await tab.click(); await page.waitForTimeout(600); } catch {}
}
// Status filter buttons
const statusBtns = await page.$$('#nodeStatusFilter .btn, [data-status]');
for (const btn of statusBtns) {
try { await btn.click(); await page.waitForTimeout(400); } catch {}
}
// Use search box
await safeFill('#nodeSearch', 'test');
await page.waitForTimeout(500);
await safeFill('#nodeSearch', '');
await page.waitForTimeout(300);
// Use dropdowns (Last Heard, etc.)
const selects = await page.$$('select');
for (const sel of selects) {
try {
const options = await sel.$$eval('option', opts => opts.map(o => o.value));
if (options.length > 1) {
await sel.selectOption(options[1]);
await page.waitForTimeout(400);
if (options.length > 2) {
await sel.selectOption(options[2]);
await page.waitForTimeout(400);
}
await sel.selectOption(options[0]);
await page.waitForTimeout(300);
}
} catch {}
}
// Click node rows to open side pane
const nodeRows = await page.$$('table tbody tr');
for (let i = 0; i < Math.min(nodeRows.length, 3); i++) {
try { await nodeRows[i].click(); await page.waitForTimeout(600); } catch {}
}
// Click Details link in side pane
await safeClick('a[href*="node/"]', 2000);
await page.waitForTimeout(1500);
// If on node detail page, interact with it
try {
// Click tabs on detail page if any
const detailTabs = await page.$$('.tab-btn, [data-tab]');
for (const tab of detailTabs) {
try { await tab.click(); await page.waitForTimeout(400); } catch {}
}
} catch {}
// Go back to nodes
await page.goto(`${BASE}/#/nodes`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
await page.waitForTimeout(1500);
// ── PACKETS PAGE ──
console.log(' [coverage] Packets page...');
await page.goto(`${BASE}/#/packets`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
await page.waitForTimeout(2000);
// Type filter expressions
const filterInput = await page.$('#packetFilterInput');
if (filterInput) {
const filters = ['type == ADVERT', 'type == GRP_TXT', 'hops > 1', 'type == GRP_TXT && hops > 1', 'rssi < -80', 'snr > 5', 'type == TXT_MSG', ''];
for (const f of filters) {
try {
await filterInput.fill(f);
await page.waitForTimeout(600);
} catch {}
}
}
// Click Group by Hash button
await safeClick('#fGroup');
await page.waitForTimeout(800);
// Click packet rows (group headers)
const packetRows = await page.$$('table tbody tr');
for (let i = 0; i < Math.min(packetRows.length, 5); i++) {
try { await packetRows[i].click(); await page.waitForTimeout(500); } catch {}
}
// Toggle group off
await safeClick('#fGroup');
await page.waitForTimeout(800);
// Change time window dropdown
const pktSelects = await page.$$('select');
for (const sel of pktSelects) {
try {
const options = await sel.$$eval('option', opts => opts.map(o => ({ value: o.value, text: o.textContent })));
const isTimeSelect = options.some(o => o.text.match(/hour|min|day|all|time/i));
if (isTimeSelect) {
for (let i = 0; i < Math.min(options.length, 4); i++) {
await sel.selectOption(options[i].value);
await page.waitForTimeout(500);
}
}
} catch {}
}
// ── MAP PAGE ──
console.log(' [coverage] Map page...');
await page.goto(`${BASE}/#/map`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
await page.waitForTimeout(3000);
// Click markers
const markers = await page.$$('.leaflet-marker-icon, .leaflet-interactive');
for (let i = 0; i < Math.min(markers.length, 3); i++) {
try { await markers[i].click(); await page.waitForTimeout(500); } catch {}
}
// Toggle filter checkboxes in legend
const checkboxes = await page.$$('input[type="checkbox"]');
for (const cb of checkboxes) {
try {
await cb.click(); await page.waitForTimeout(300);
await cb.click(); await page.waitForTimeout(300);
} catch {}
}
// Toggle dark mode while on map
await safeClick('#darkModeToggle');
await page.waitForTimeout(800);
await safeClick('#darkModeToggle');
await page.waitForTimeout(500);
// ── ANALYTICS PAGE ──
console.log(' [coverage] Analytics page...');
await page.goto(`${BASE}/#/analytics`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
await page.waitForTimeout(3000); // wait for data to load
// Click ALL analytics tabs using specific selector
const tabNames = ['overview', 'rf', 'topology', 'channels', 'hashsizes', 'collisions', 'subpaths', 'nodes', 'distance'];
for (const tabName of tabNames) {
try {
await page.click(`#analyticsTabs [data-tab="${tabName}"]`, { timeout: 2000 });
await page.waitForTimeout(1200); // give renderTab time
} catch {}
}
// Also test deep-link tabs
for (const tab of ['collisions', 'rf', 'distance', 'topology', 'nodes', 'subpaths']) {
await page.goto(`${BASE}/#/analytics?tab=${tab}`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
await page.waitForTimeout(2000);
}
// Exercise some interactions
// ── CUSTOMIZE ──
console.log(' [coverage] Customizer...');
await page.goto(BASE, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
await page.waitForTimeout(500);
await safeClick('#customizeToggle');
await page.waitForTimeout(1000);
// Click each customizer tab using specific selector
const custTabs = await page.$$('.cust-tab[data-tab]');
for (const tab of custTabs) {
try { await tab.click(); await page.waitForTimeout(500); } catch {}
}
// Click preset theme buttons
const presets = await page.$$('.cust-preset-btn[data-preset]');
for (let i = 0; i < Math.min(presets.length, 4); i++) {
try { await presets[i].click(); await page.waitForTimeout(400); } catch {}
}
// Change a color input
const colorInputs = await page.$$('input[type="color"]');
for (let i = 0; i < Math.min(colorInputs.length, 3); i++) {
try {
await colorInputs[i].evaluate(el => { el.value = '#ff0000'; el.dispatchEvent(new Event('input', {bubbles:true})); });
await page.waitForTimeout(300);
} catch {}
}
// Reset preview
await safeClick('#custResetPreview');
await page.waitForTimeout(400);
// Reset user theme
await safeClick('#custResetUser');
await page.waitForTimeout(400);
// Close customizer
await safeClick('#customizeToggle');
// ── CHANNELS PAGE ──
console.log(' [coverage] Channels page...');
await page.goto(`${BASE}/#/channels`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
await page.waitForTimeout(2000);
// Click any channel items
const channelItems = await page.$$('.channel-item, .channel-row, .channel-card, tr td');
if (channelItems.length > 0) {
try { await channelItems[0].click(); await page.waitForTimeout(600); } catch {}
}
// ── LIVE PAGE ──
console.log(' [coverage] Live page...');
await page.goto(`${BASE}/#/live`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
await page.waitForTimeout(3000);
// Click live controls if any
const liveButtons = await page.$$('button');
for (const btn of liveButtons) {
const text = await btn.textContent().catch(() => '');
if (text.match(/pause|Pause|resume|Resume|clear|Clear|vcr|VCR/i)) {
try { await btn.click(); await page.waitForTimeout(400); } catch {}
}
}
// ── TRACES PAGE ──
console.log(' [coverage] Traces page...');
await page.goto(`${BASE}/#/traces`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
await page.waitForTimeout(2000);
// ── OBSERVERS PAGE ──
console.log(' [coverage] Observers page...');
await page.goto(`${BASE}/#/observers`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
await page.waitForTimeout(2000);
// Click observer rows
const obsRows = await page.$$('table tbody tr, .observer-card, .observer-row');
for (let i = 0; i < Math.min(obsRows.length, 2); i++) {
try { await obsRows[i].click(); await page.waitForTimeout(500); } catch {}
}
// ── GLOBAL SEARCH ──
console.log(' [coverage] Global search...');
await safeClick('#searchToggle');
await page.waitForTimeout(500);
await safeFill('#searchInput', 'test');
await page.waitForTimeout(800);
await safeFill('#searchInput', '');
await page.keyboard.press('Escape').catch(() => {});
await page.waitForTimeout(300);
// ── FAVORITES ──
await safeClick('#favToggle');
await page.waitForTimeout(400);
await safeClick('#favToggle');
await page.waitForTimeout(300);
// ── DARK MODE TOGGLE ──
await safeClick('#darkModeToggle');
await page.waitForTimeout(500);
// ── KEYBOARD SHORTCUT (Ctrl+K for search) ──
try {
await page.click('#customizeToggle');
await page.waitForTimeout(1000);
await page.keyboard.press('Control+k');
await page.waitForTimeout(500);
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
} catch {}
// ── Navigate via nav links to exercise router ──
console.log(' [coverage] Nav link navigation...');
const routes = ['home', 'packets', 'map', 'live', 'channels', 'nodes', 'traces', 'observers', 'analytics', 'perf'];
for (const route of routes) {
await safeClick(`a[data-route="${route}"]`);
await page.waitForTimeout(1000);
}
// Extract coverage
const coverage = await page.evaluate(() => window.__coverage__);
await browser.close();

View File

@@ -15,6 +15,7 @@ sleep 5
# 4. Run Playwright tests (exercises frontend code)
BASE_URL=http://localhost:13581 node test-e2e-playwright.js || true
BASE_URL=http://localhost:13581 node test-e2e-interactions.js || true
# 5. Collect browser coverage
BASE_URL=http://localhost:13581 node scripts/collect-frontend-coverage.js

View File

@@ -378,7 +378,7 @@ async function main() {
const msgResp = (await get(`/api/channels/${someCh.hash}/messages`)).data;
assert(msgResp.messages.length > 0, 'channel has message list');
assert(msgResp.messages[0].sender !== undefined, 'message has sender');
console.log(` ✓ Channels: ${chResp.channels.length} channels\n`);
console.log(` ✓ Channels: ${chList.length} channels\n`);
} else {
console.log(` ⚠ Channels: 0 (synthetic packets don't produce decodable channel messages)\n`);
}