Files
meshcore-analyzer/scripts/collect-frontend-coverage.js
you f4020f43cd feat(coverage): add targeted Playwright interactions for higher frontend coverage
Add redundant selectors (data-sort, data-role, data-status, data-tab,
placeholder-based search, emoji theme toggle, .cust-close, #fTimeWindow,
broader preset selectors) to exercise more frontend code paths.

All interactions wrapped in try/catch for resilience.
2026-03-24 04:19:32 +00:00

426 lines
16 KiB
JavaScript

// After Playwright tests, this script:
// 1. Connects to the running test server
// 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');
const path = require('path');
async function collectCoverage() {
const browser = await chromium.launch({
executablePath: process.env.CHROMIUM_PATH || '/usr/bin/chromium',
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';
// 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 sort headers by data-sort attribute
try { await page.click('th[data-sort="name"]'); await page.waitForTimeout(300); } catch {}
try { await page.click('th[data-sort="last_seen"]'); 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 {}
}
// Click role tabs by data-role attribute
try { await page.click('[data-role="repeater"]'); await page.waitForTimeout(500); } catch {}
try { await page.click('[data-role="all"]'); await page.waitForTimeout(300); } 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 {}
}
// Click status filters by data-status attribute
try { await page.click('[data-status="active"]'); await page.waitForTimeout(300); } catch {}
try { await page.click('[data-status="all"]'); await page.waitForTimeout(300); } catch {}
// Use search box
await safeFill('#nodeSearch', 'test');
await page.waitForTimeout(500);
await safeFill('#nodeSearch', '');
await page.waitForTimeout(300);
// Search by placeholder
try { await page.fill('input[placeholder*="Search"]', 'test'); await page.waitForTimeout(500); } catch {}
try { await page.fill('input[placeholder*="Search"]', ''); await page.waitForTimeout(300); } catch {};
// 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 {}
}
}
// Direct filter interactions
try { await page.fill('#packetFilterInput', 'type == ADVERT'); await page.waitForTimeout(500); } catch {}
try { await page.fill('#packetFilterInput', ''); await page.waitForTimeout(300); } catch {}
// Change time window via select
try { await page.selectOption('#fTimeWindow', '60'); await page.waitForTimeout(500); } catch {}
// Click a packet row
try {
const pktRows2 = await page.$$('table tbody tr');
if (pktRows2.length) { await pktRows2[0].click(); await page.waitForTimeout(1000); }
} 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 role filters by data-role
try { await page.click('input[data-role="companion"]'); await page.waitForTimeout(300); } catch {}
try { await page.click('input[data-role="companion"]'); await page.waitForTimeout(300); } catch {}
// Status filter on map
try { await page.click('[data-status="active"]'); await page.waitForTimeout(300); } catch {}
try { await page.click('[data-status="all"]'); 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 {}
}
// Click analytics tabs by data-tab directly (broader selector)
for (const tab of ['rf', 'topology', 'channels', 'hashsizes', 'collisions', 'subpaths', 'nodes', 'distance']) {
try { await page.click(`[data-tab="${tab}"]`); await page.waitForTimeout(800); } 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);
}
// ── 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 {}
}
// Click presets by broader selector
try {
const presets2 = await page.$$('.cust-preset, [data-preset]');
if (presets2.length) { await presets2[0].click(); await page.waitForTimeout(500); }
} 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');
// Re-open customizer and use broader selectors
try { await page.click('#customizeToggle'); await page.waitForTimeout(500); } catch {}
for (const tab of ['branding', 'theme', 'nodes', 'home', 'export']) {
try { await page.click(`[data-tab="${tab}"]`); await page.waitForTimeout(300); } catch {}
}
try { await page.click('.cust-close'); await page.waitForTimeout(300); } catch {}
// ── 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);
// Toggle via emoji button selector
try {
const themeBtn = await page.$('button:has-text("☀️"), button:has-text("🌙")');
if (themeBtn) { await themeBtn.click(); await page.waitForTimeout(500); }
} catch {}
// ── KEYBOARD SHORTCUT (Ctrl+K for search) ──
try {
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();
if (coverage) {
const outDir = path.join(__dirname, '..', '.nyc_output');
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
fs.writeFileSync(path.join(outDir, 'frontend-coverage.json'), JSON.stringify(coverage));
console.log('Frontend coverage collected: ' + Object.keys(coverage).length + ' files');
} else {
console.log('WARNING: No __coverage__ object found — instrumentation may have failed');
}
}
collectCoverage().catch(e => { console.error(e); process.exit(1); });