mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-09-27 07:09:45 +00:00
Exercise every major code path across all frontend files: app.js: all routes, bad routes, hashchange, theme toggle x4, hamburger menu, favorites dropdown, global search, Ctrl+K, apiPerf(), timeAgo/truncate/routeTypeName utils nodes.js: sort every column (both directions), every role tab, every status filter, cycle all Last Heard options, click rows for side pane, navigate to detail page, copy URL, show all paths, node analytics day buttons (1/7/30/365), scroll target packets.js: 12 filter expressions including bad ones, cycle all time windows, group by hash toggle, My Nodes toggle, observer menu, type filter menu, hash input, node filter, observer sort, column toggle menu, hex hash toggle, pause button, resize handle, deep-link to packet hash map.js: all role checkboxes toggle, clusters/heatmap/neighbors/ hash labels toggles, cycle Last Heard, status filter buttons, jump buttons, markers, zoom controls, dark mode tile swap analytics.js: all 9 tabs clicked, deep-link to each tab via URL, observer selector on topology, navigate rows on collisions/ subpaths, sortable headers on nodes tab, region filter customize.js: all 5 tabs, all preset themes, branding text inputs, theme color inputs, node color inputs, type color inputs, reset buttons, home tab fields (hero, journey steps, checklist, links), export tab, reset preview/user theme live.js: VCR pause/speed/missed/prompt buttons, all visualization toggles (heat/ghost/realistic/favorites/matrix/rain), audio toggle + BPM slider, timeline click, resize event channels.js: click rows, navigate to specific channel observers.js: click rows, navigate to detail, cycle days select traces.js: click rows perf.js: refresh + reset buttons home.js: both chooser paths, search + suggest, my-node cards, health/packets buttons, remove buttons, toggle level, timeline Also exercises packet-filter parser and region-filter directly.
979 lines
35 KiB
JavaScript
979 lines
35 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 || undefined,
|
|
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(300);
|
|
} catch {}
|
|
}
|
|
|
|
// Helper: safe fill
|
|
async function safeFill(selector, text) {
|
|
try {
|
|
await page.fill(selector, text);
|
|
await page.waitForTimeout(300);
|
|
} catch {}
|
|
}
|
|
|
|
// Helper: safe select
|
|
async function safeSelect(selector, value) {
|
|
try {
|
|
await page.selectOption(selector, value);
|
|
await page.waitForTimeout(300);
|
|
} catch {}
|
|
}
|
|
|
|
// Helper: click all matching elements
|
|
async function clickAll(selector, max = 10) {
|
|
try {
|
|
const els = await page.$$(selector);
|
|
for (let i = 0; i < Math.min(els.length, max); i++) {
|
|
try { await els[i].click(); await page.waitForTimeout(300); } catch {}
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
// Helper: iterate all select options
|
|
async function cycleSelect(selector) {
|
|
try {
|
|
const options = await page.$$eval(`${selector} option`, opts => opts.map(o => o.value));
|
|
for (const val of options) {
|
|
try { await page.selectOption(selector, val); await page.waitForTimeout(400); } catch {}
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
// ══════════════════════════════════════════════
|
|
// HOME PAGE
|
|
// ══════════════════════════════════════════════
|
|
console.log(' [coverage] Home page — chooser...');
|
|
// Clear localStorage to get chooser
|
|
await page.goto(BASE, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.evaluate(() => localStorage.clear()).catch(() => {});
|
|
await page.goto(`${BASE}/#/home`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(1500);
|
|
|
|
// Click "I'm new"
|
|
await safeClick('#chooseNew');
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Now on home page as "new" user — interact with search
|
|
await safeFill('#homeSearch', 'test');
|
|
await page.waitForTimeout(600);
|
|
// Click suggest items if any
|
|
await clickAll('.suggest-item', 3);
|
|
// Click suggest claim buttons
|
|
await clickAll('.suggest-claim', 2);
|
|
await safeFill('#homeSearch', '');
|
|
await page.waitForTimeout(300);
|
|
|
|
// Click my-node-card elements
|
|
await clickAll('.my-node-card', 3);
|
|
await page.waitForTimeout(300);
|
|
// Click health/packets buttons on cards
|
|
await clickAll('[data-action="health"]', 2);
|
|
await page.waitForTimeout(500);
|
|
await clickAll('[data-action="packets"]', 2);
|
|
await page.waitForTimeout(500);
|
|
|
|
// Click toggle level
|
|
await safeClick('#toggleLevel');
|
|
await page.waitForTimeout(500);
|
|
|
|
// Click FAQ items
|
|
await clickAll('.faq-q, .question, [class*="accordion"]', 5);
|
|
|
|
// Click timeline items
|
|
await clickAll('.timeline-item', 5);
|
|
|
|
// Click health claim button
|
|
await clickAll('.health-claim', 2);
|
|
|
|
// Click cards
|
|
await clickAll('.card, .health-card', 3);
|
|
|
|
// Click remove buttons on my-node cards
|
|
await clickAll('.mnc-remove', 2);
|
|
|
|
// Switch to experienced mode
|
|
await page.evaluate(() => localStorage.clear()).catch(() => {});
|
|
await page.goto(`${BASE}/#/home`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(1000);
|
|
await safeClick('#chooseExp');
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Interact with experienced home page
|
|
await safeFill('#homeSearch', 'a');
|
|
await page.waitForTimeout(600);
|
|
await clickAll('.suggest-item', 2);
|
|
await safeFill('#homeSearch', '');
|
|
await page.waitForTimeout(300);
|
|
|
|
// Click outside to dismiss suggest
|
|
await page.evaluate(() => document.body.click()).catch(() => {});
|
|
await page.waitForTimeout(300);
|
|
|
|
// ══════════════════════════════════════════════
|
|
// NODES PAGE
|
|
// ══════════════════════════════════════════════
|
|
console.log(' [coverage] Nodes page...');
|
|
await page.goto(`${BASE}/#/nodes`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Sort by EVERY column
|
|
for (const col of ['name', 'public_key', 'role', 'last_seen', 'advert_count']) {
|
|
try { await page.click(`th[data-sort="${col}"]`); await page.waitForTimeout(300); } catch {}
|
|
// Click again for reverse sort
|
|
try { await page.click(`th[data-sort="${col}"]`); await page.waitForTimeout(300); } catch {}
|
|
}
|
|
|
|
// Click EVERY role tab
|
|
const roleTabs = await page.$$('.node-tab[data-tab]');
|
|
for (const tab of roleTabs) {
|
|
try { await tab.click(); await page.waitForTimeout(500); } catch {}
|
|
}
|
|
// Go back to "all"
|
|
try { await page.click('.node-tab[data-tab="all"]'); await page.waitForTimeout(400); } catch {}
|
|
|
|
// Click EVERY status filter
|
|
for (const status of ['active', 'stale', 'all']) {
|
|
try { await page.click(`#nodeStatusFilter .btn[data-status="${status}"]`); await page.waitForTimeout(400); } catch {}
|
|
}
|
|
|
|
// Cycle EVERY Last Heard option
|
|
await cycleSelect('#nodeLastHeard');
|
|
|
|
// Search
|
|
await safeFill('#nodeSearch', 'test');
|
|
await page.waitForTimeout(500);
|
|
await safeFill('#nodeSearch', '');
|
|
await page.waitForTimeout(300);
|
|
|
|
// Click node rows to open side pane — try multiple
|
|
const nodeRows = await page.$$('#nodesBody tr');
|
|
for (let i = 0; i < Math.min(nodeRows.length, 4); i++) {
|
|
try { await nodeRows[i].click(); await page.waitForTimeout(600); } catch {}
|
|
}
|
|
|
|
// In side pane — click detail/analytics links
|
|
await safeClick('a[href*="/nodes/"]', 2000);
|
|
await page.waitForTimeout(1500);
|
|
// Click fav star
|
|
await clickAll('.fav-star', 2);
|
|
|
|
// On node detail page — interact
|
|
// Click back button
|
|
await safeClick('#nodeBackBtn');
|
|
await page.waitForTimeout(500);
|
|
|
|
// Navigate to a node detail page via hash
|
|
try {
|
|
const firstNodeKey = await page.$eval('#nodesBody tr td:nth-child(2)', el => el.textContent.trim());
|
|
if (firstNodeKey) {
|
|
await page.goto(`${BASE}/#/nodes/${firstNodeKey}`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Click tabs on detail page
|
|
await clickAll('.tab-btn, [data-tab]', 10);
|
|
|
|
// Click copy URL button
|
|
await safeClick('#copyUrlBtn');
|
|
|
|
// Click "Show all paths" button
|
|
await safeClick('#showAllPaths');
|
|
await safeClick('#showAllFullPaths');
|
|
|
|
// Click node analytics day buttons
|
|
for (const days of ['1', '7', '30', '365']) {
|
|
try { await page.click(`[data-days="${days}"]`); await page.waitForTimeout(800); } catch {}
|
|
}
|
|
}
|
|
} catch {}
|
|
|
|
// Node detail with scroll target
|
|
try {
|
|
const firstKey = await page.$eval('#nodesBody tr td:nth-child(2)', el => el.textContent.trim()).catch(() => null);
|
|
if (firstKey) {
|
|
await page.goto(`${BASE}/#/nodes/${firstKey}?scroll=paths`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(1500);
|
|
}
|
|
} catch {}
|
|
|
|
// ══════════════════════════════════════════════
|
|
// PACKETS PAGE
|
|
// ══════════════════════════════════════════════
|
|
console.log(' [coverage] Packets page...');
|
|
await page.goto(`${BASE}/#/packets`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Open filter bar
|
|
await safeClick('#filterToggleBtn');
|
|
await page.waitForTimeout(500);
|
|
|
|
// Type various filter expressions
|
|
const filterExprs = [
|
|
'type == ADVERT', 'type == GRP_TXT', 'snr > 0', 'hops > 1',
|
|
'route == FLOOD', 'rssi < -80', 'type == TXT_MSG', 'type == ACK',
|
|
'snr > 5 && hops > 1', 'type == PATH', '@@@', ''
|
|
];
|
|
for (const expr of filterExprs) {
|
|
await safeFill('#packetFilterInput', expr);
|
|
await page.waitForTimeout(500);
|
|
}
|
|
|
|
// Cycle ALL time window options
|
|
await cycleSelect('#fTimeWindow');
|
|
|
|
// Toggle group by hash
|
|
await safeClick('#fGroup');
|
|
await page.waitForTimeout(600);
|
|
await safeClick('#fGroup');
|
|
await page.waitForTimeout(600);
|
|
|
|
// Toggle My Nodes filter
|
|
await safeClick('#fMyNodes');
|
|
await page.waitForTimeout(500);
|
|
await safeClick('#fMyNodes');
|
|
await page.waitForTimeout(500);
|
|
|
|
// Click observer menu trigger
|
|
await safeClick('#observerTrigger');
|
|
await page.waitForTimeout(400);
|
|
// Click items in observer menu
|
|
await clickAll('#observerMenu input[type="checkbox"]', 5);
|
|
await safeClick('#observerTrigger');
|
|
await page.waitForTimeout(300);
|
|
|
|
// Click type filter trigger
|
|
await safeClick('#typeTrigger');
|
|
await page.waitForTimeout(400);
|
|
await clickAll('#typeMenu input[type="checkbox"]', 5);
|
|
await safeClick('#typeTrigger');
|
|
await page.waitForTimeout(300);
|
|
|
|
// Hash input
|
|
await safeFill('#fHash', 'abc123');
|
|
await page.waitForTimeout(500);
|
|
await safeFill('#fHash', '');
|
|
await page.waitForTimeout(300);
|
|
|
|
// Node filter
|
|
await safeFill('#fNode', 'test');
|
|
await page.waitForTimeout(500);
|
|
await clickAll('.node-filter-option', 3);
|
|
await safeFill('#fNode', '');
|
|
await page.waitForTimeout(300);
|
|
|
|
// Observer sort
|
|
await cycleSelect('#fObsSort');
|
|
|
|
// Column toggle menu
|
|
await safeClick('#colToggleBtn');
|
|
await page.waitForTimeout(400);
|
|
await clickAll('#colToggleMenu input[type="checkbox"]', 8);
|
|
await safeClick('#colToggleBtn');
|
|
await page.waitForTimeout(300);
|
|
|
|
// Hex hash toggle
|
|
await safeClick('#hexHashToggle');
|
|
await page.waitForTimeout(400);
|
|
await safeClick('#hexHashToggle');
|
|
await page.waitForTimeout(300);
|
|
|
|
// Pause button
|
|
await safeClick('#pktPauseBtn');
|
|
await page.waitForTimeout(400);
|
|
await safeClick('#pktPauseBtn');
|
|
await page.waitForTimeout(400);
|
|
|
|
// Click packet rows to open detail pane
|
|
const pktRows = await page.$$('#pktBody tr');
|
|
for (let i = 0; i < Math.min(pktRows.length, 5); i++) {
|
|
try { await pktRows[i].click(); await page.waitForTimeout(500); } catch {}
|
|
}
|
|
|
|
// Resize handle drag simulation
|
|
try {
|
|
await page.evaluate(() => {
|
|
const handle = document.getElementById('pktResizeHandle');
|
|
if (handle) {
|
|
handle.dispatchEvent(new MouseEvent('mousedown', { clientX: 500, bubbles: true }));
|
|
document.dispatchEvent(new MouseEvent('mousemove', { clientX: 400, bubbles: true }));
|
|
document.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
|
|
}
|
|
});
|
|
await page.waitForTimeout(300);
|
|
} catch {}
|
|
|
|
// Click outside filter menus to close them
|
|
try {
|
|
await page.evaluate(() => document.body.click());
|
|
await page.waitForTimeout(300);
|
|
} catch {}
|
|
|
|
// Navigate to specific packet by hash
|
|
await page.goto(`${BASE}/#/packets/deadbeef`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(1500);
|
|
|
|
// ══════════════════════════════════════════════
|
|
// MAP PAGE
|
|
// ══════════════════════════════════════════════
|
|
console.log(' [coverage] Map page...');
|
|
await page.goto(`${BASE}/#/map`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(3000);
|
|
|
|
// Toggle controls panel
|
|
await safeClick('#mapControlsToggle');
|
|
await page.waitForTimeout(500);
|
|
|
|
// Toggle each role checkbox on/off
|
|
try {
|
|
const roleChecks = await page.$$('#mcRoleChecks input[type="checkbox"]');
|
|
for (const cb of roleChecks) {
|
|
try { await cb.click(); await page.waitForTimeout(300); } catch {}
|
|
try { await cb.click(); await page.waitForTimeout(300); } catch {}
|
|
}
|
|
} catch {}
|
|
|
|
// Toggle clusters, heatmap, neighbors, hash labels
|
|
await safeClick('#mcClusters');
|
|
await page.waitForTimeout(300);
|
|
await safeClick('#mcClusters');
|
|
await page.waitForTimeout(300);
|
|
await safeClick('#mcHeatmap');
|
|
await page.waitForTimeout(300);
|
|
await safeClick('#mcHeatmap');
|
|
await page.waitForTimeout(300);
|
|
await safeClick('#mcNeighbors');
|
|
await page.waitForTimeout(300);
|
|
await safeClick('#mcNeighbors');
|
|
await page.waitForTimeout(300);
|
|
await safeClick('#mcHashLabels');
|
|
await page.waitForTimeout(300);
|
|
await safeClick('#mcHashLabels');
|
|
await page.waitForTimeout(300);
|
|
|
|
// Last heard dropdown on map
|
|
await cycleSelect('#mcLastHeard');
|
|
|
|
// Status filter buttons on map
|
|
for (const st of ['active', 'stale', 'all']) {
|
|
try { await page.click(`#mcStatusFilter .btn[data-status="${st}"]`); await page.waitForTimeout(400); } catch {}
|
|
}
|
|
|
|
// Click jump buttons (region jumps)
|
|
await clickAll('#mcJumps button', 5);
|
|
|
|
// Click markers
|
|
await clickAll('.leaflet-marker-icon', 5);
|
|
await clickAll('.leaflet-interactive', 3);
|
|
|
|
// Click popups
|
|
await clickAll('.leaflet-popup-content a', 3);
|
|
|
|
// Zoom controls
|
|
await safeClick('.leaflet-control-zoom-in');
|
|
await page.waitForTimeout(300);
|
|
await safeClick('.leaflet-control-zoom-out');
|
|
await page.waitForTimeout(300);
|
|
|
|
// Toggle dark mode while on map (triggers tile layer swap)
|
|
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);
|
|
|
|
// Click EVERY analytics tab
|
|
const analyticsTabs = ['overview', 'rf', 'topology', 'channels', 'hashsizes', 'collisions', 'subpaths', 'nodes', 'distance'];
|
|
for (const tabName of analyticsTabs) {
|
|
try {
|
|
await page.click(`#analyticsTabs [data-tab="${tabName}"]`, { timeout: 2000 });
|
|
await page.waitForTimeout(1500);
|
|
} catch {}
|
|
}
|
|
|
|
// On topology tab — click observer selector buttons
|
|
try {
|
|
await page.click('#analyticsTabs [data-tab="topology"]', { timeout: 2000 });
|
|
await page.waitForTimeout(1500);
|
|
await clickAll('#obsSelector .tab-btn', 5);
|
|
// Click the "All Observers" button
|
|
await safeClick('[data-obs="__all"]');
|
|
await page.waitForTimeout(500);
|
|
} catch {}
|
|
|
|
// On collisions tab — click navigate rows
|
|
try {
|
|
await page.click('#analyticsTabs [data-tab="collisions"]', { timeout: 2000 });
|
|
await page.waitForTimeout(1500);
|
|
await clickAll('tr[data-action="navigate"]', 3);
|
|
await page.waitForTimeout(500);
|
|
} catch {}
|
|
|
|
// On subpaths tab — click rows
|
|
try {
|
|
await page.click('#analyticsTabs [data-tab="subpaths"]', { timeout: 2000 });
|
|
await page.waitForTimeout(1500);
|
|
await clickAll('tr[data-action="navigate"]', 3);
|
|
await page.waitForTimeout(500);
|
|
} catch {}
|
|
|
|
// On nodes tab — click sortable headers
|
|
try {
|
|
await page.click('#analyticsTabs [data-tab="nodes"]', { timeout: 2000 });
|
|
await page.waitForTimeout(1500);
|
|
await clickAll('.analytics-table th', 8);
|
|
await page.waitForTimeout(300);
|
|
} catch {}
|
|
|
|
// Deep-link to each analytics tab via URL
|
|
for (const tab of analyticsTabs) {
|
|
await page.goto(`${BASE}/#/analytics?tab=${tab}`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(1500);
|
|
}
|
|
|
|
// Region filter on analytics
|
|
try {
|
|
await page.click('#analyticsRegionFilter');
|
|
await page.waitForTimeout(300);
|
|
await clickAll('#analyticsRegionFilter input[type="checkbox"]', 3);
|
|
await page.waitForTimeout(300);
|
|
} catch {}
|
|
|
|
// ══════════════════════════════════════════════
|
|
// 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 EVERY customizer tab
|
|
for (const tab of ['branding', 'theme', 'nodes', 'home', 'export']) {
|
|
try { await page.click(`.cust-tab[data-tab="${tab}"]`); await page.waitForTimeout(500); } catch {}
|
|
}
|
|
|
|
// On branding tab — change text inputs
|
|
try {
|
|
await page.click('.cust-tab[data-tab="branding"]');
|
|
await page.waitForTimeout(300);
|
|
await safeFill('input[data-key="branding.siteName"]', 'Test Site');
|
|
await safeFill('input[data-key="branding.tagline"]', 'Test Tagline');
|
|
await safeFill('input[data-key="branding.logoUrl"]', 'https://example.com/logo.png');
|
|
await safeFill('input[data-key="branding.faviconUrl"]', 'https://example.com/favicon.ico');
|
|
} catch {}
|
|
|
|
// On theme tab — click EVERY preset
|
|
try {
|
|
await page.click('.cust-tab[data-tab="theme"]');
|
|
await page.waitForTimeout(300);
|
|
const presets = await page.$$('.cust-preset-btn[data-preset]');
|
|
for (const preset of presets) {
|
|
try { await preset.click(); await page.waitForTimeout(400); } catch {}
|
|
}
|
|
} catch {}
|
|
|
|
// Change color inputs on theme tab
|
|
try {
|
|
const colorInputs = await page.$$('input[type="color"][data-theme]');
|
|
for (let i = 0; i < Math.min(colorInputs.length, 5); i++) {
|
|
try {
|
|
await colorInputs[i].evaluate(el => {
|
|
el.value = '#ff5500';
|
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
});
|
|
await page.waitForTimeout(200);
|
|
} catch {}
|
|
}
|
|
} catch {}
|
|
|
|
// Click reset buttons on theme
|
|
await clickAll('[data-reset-theme]', 3);
|
|
await clickAll('[data-reset-node]', 3);
|
|
await clickAll('[data-reset-type]', 3);
|
|
|
|
// On nodes tab — change node color inputs
|
|
try {
|
|
await page.click('.cust-tab[data-tab="nodes"]');
|
|
await page.waitForTimeout(300);
|
|
const nodeColors = await page.$$('input[type="color"][data-node]');
|
|
for (let i = 0; i < Math.min(nodeColors.length, 3); i++) {
|
|
try {
|
|
await nodeColors[i].evaluate(el => {
|
|
el.value = '#00ff00';
|
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
});
|
|
await page.waitForTimeout(200);
|
|
} catch {}
|
|
}
|
|
// Type color inputs
|
|
const typeColors = await page.$$('input[type="color"][data-type-color]');
|
|
for (let i = 0; i < Math.min(typeColors.length, 3); i++) {
|
|
try {
|
|
await typeColors[i].evaluate(el => {
|
|
el.value = '#0000ff';
|
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
});
|
|
await page.waitForTimeout(200);
|
|
} catch {}
|
|
}
|
|
} catch {}
|
|
|
|
// On home tab — edit home customization fields
|
|
try {
|
|
await page.click('.cust-tab[data-tab="home"]');
|
|
await page.waitForTimeout(300);
|
|
await safeFill('input[data-key="home.heroTitle"]', 'Test Hero');
|
|
await safeFill('input[data-key="home.heroSubtitle"]', 'Test Subtitle');
|
|
// Edit journey steps
|
|
await clickAll('[data-move-step]', 2);
|
|
await clickAll('[data-rm-step]', 1);
|
|
// Edit checklist
|
|
await clickAll('[data-rm-check]', 1);
|
|
// Edit links
|
|
await clickAll('[data-rm-link]', 1);
|
|
// Modify step fields
|
|
const stepTitles = await page.$$('input[data-step-field="title"]');
|
|
for (let i = 0; i < Math.min(stepTitles.length, 2); i++) {
|
|
try {
|
|
await stepTitles[i].fill('Test Step ' + i);
|
|
await page.waitForTimeout(200);
|
|
} catch {}
|
|
}
|
|
} catch {}
|
|
|
|
// On export tab
|
|
try {
|
|
await page.click('.cust-tab[data-tab="export"]');
|
|
await page.waitForTimeout(500);
|
|
// Click export/import buttons if present
|
|
await clickAll('.cust-panel[data-panel="export"] button', 3);
|
|
} catch {}
|
|
|
|
// Reset preview and user theme
|
|
await safeClick('#custResetPreview');
|
|
await page.waitForTimeout(400);
|
|
await safeClick('#custResetUser');
|
|
await page.waitForTimeout(400);
|
|
|
|
// Close customizer
|
|
await safeClick('.cust-close');
|
|
await page.waitForTimeout(300);
|
|
|
|
// ══════════════════════════════════════════════
|
|
// CHANNELS PAGE
|
|
// ══════════════════════════════════════════════
|
|
console.log(' [coverage] Channels page...');
|
|
await page.goto(`${BASE}/#/channels`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(2000);
|
|
// Click channel rows/items
|
|
await clickAll('.channel-item, .channel-row, .channel-card', 3);
|
|
await clickAll('table tbody tr', 3);
|
|
|
|
// Navigate to a specific channel
|
|
try {
|
|
const channelHash = await page.$eval('table tbody tr td:first-child', el => el.textContent.trim()).catch(() => null);
|
|
if (channelHash) {
|
|
await page.goto(`${BASE}/#/channels/${channelHash}`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(1500);
|
|
}
|
|
} catch {}
|
|
|
|
// ══════════════════════════════════════════════
|
|
// LIVE PAGE
|
|
// ══════════════════════════════════════════════
|
|
console.log(' [coverage] Live page...');
|
|
await page.goto(`${BASE}/#/live`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(3000);
|
|
|
|
// VCR controls
|
|
await safeClick('#vcrPauseBtn');
|
|
await page.waitForTimeout(400);
|
|
await safeClick('#vcrPauseBtn');
|
|
await page.waitForTimeout(400);
|
|
|
|
// VCR speed cycle
|
|
await safeClick('#vcrSpeedBtn');
|
|
await page.waitForTimeout(300);
|
|
await safeClick('#vcrSpeedBtn');
|
|
await page.waitForTimeout(300);
|
|
await safeClick('#vcrSpeedBtn');
|
|
await page.waitForTimeout(300);
|
|
|
|
// VCR mode / missed
|
|
await safeClick('#vcrMissed');
|
|
await page.waitForTimeout(300);
|
|
|
|
// VCR prompt buttons
|
|
await safeClick('#vcrPromptReplay');
|
|
await page.waitForTimeout(300);
|
|
await safeClick('#vcrPromptSkip');
|
|
await page.waitForTimeout(300);
|
|
|
|
// Toggle visualization options
|
|
await safeClick('#liveHeatToggle');
|
|
await page.waitForTimeout(400);
|
|
await safeClick('#liveHeatToggle');
|
|
await page.waitForTimeout(300);
|
|
|
|
await safeClick('#liveGhostToggle');
|
|
await page.waitForTimeout(300);
|
|
await safeClick('#liveGhostToggle');
|
|
await page.waitForTimeout(300);
|
|
|
|
await safeClick('#liveRealisticToggle');
|
|
await page.waitForTimeout(300);
|
|
await safeClick('#liveRealisticToggle');
|
|
await page.waitForTimeout(300);
|
|
|
|
await safeClick('#liveFavoritesToggle');
|
|
await page.waitForTimeout(300);
|
|
await safeClick('#liveFavoritesToggle');
|
|
await page.waitForTimeout(300);
|
|
|
|
await safeClick('#liveMatrixToggle');
|
|
await page.waitForTimeout(300);
|
|
await safeClick('#liveMatrixToggle');
|
|
await page.waitForTimeout(300);
|
|
|
|
await safeClick('#liveMatrixRainToggle');
|
|
await page.waitForTimeout(300);
|
|
await safeClick('#liveMatrixRainToggle');
|
|
await page.waitForTimeout(300);
|
|
|
|
// Audio toggle and controls
|
|
await safeClick('#liveAudioToggle');
|
|
await page.waitForTimeout(400);
|
|
try {
|
|
await page.fill('#audioBpmSlider', '120');
|
|
await page.waitForTimeout(300);
|
|
// Dispatch input event on slider
|
|
await page.evaluate(() => {
|
|
const s = document.getElementById('audioBpmSlider');
|
|
if (s) { s.value = '140'; s.dispatchEvent(new Event('input', { bubbles: true })); }
|
|
});
|
|
await page.waitForTimeout(300);
|
|
} catch {}
|
|
await safeClick('#liveAudioToggle');
|
|
await page.waitForTimeout(300);
|
|
|
|
// VCR timeline click
|
|
try {
|
|
await page.evaluate(() => {
|
|
const canvas = document.getElementById('vcrTimeline');
|
|
if (canvas) {
|
|
const rect = canvas.getBoundingClientRect();
|
|
canvas.dispatchEvent(new MouseEvent('click', {
|
|
clientX: rect.left + rect.width * 0.5,
|
|
clientY: rect.top + rect.height * 0.5,
|
|
bubbles: true
|
|
}));
|
|
}
|
|
});
|
|
await page.waitForTimeout(500);
|
|
} catch {}
|
|
|
|
// VCR LCD canvas
|
|
try {
|
|
await page.evaluate(() => {
|
|
const canvas = document.getElementById('vcrLcdCanvas');
|
|
if (canvas) canvas.getContext('2d');
|
|
});
|
|
await page.waitForTimeout(300);
|
|
} catch {}
|
|
|
|
// Resize the live page panel
|
|
try {
|
|
await page.evaluate(() => {
|
|
window.dispatchEvent(new Event('resize'));
|
|
});
|
|
await page.waitForTimeout(300);
|
|
} catch {}
|
|
|
|
// ══════════════════════════════════════════════
|
|
// TRACES PAGE
|
|
// ══════════════════════════════════════════════
|
|
console.log(' [coverage] Traces page...');
|
|
await page.goto(`${BASE}/#/traces`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(2000);
|
|
await clickAll('table tbody tr', 3);
|
|
|
|
// ══════════════════════════════════════════════
|
|
// 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, 3); i++) {
|
|
try { await obsRows[i].click(); await page.waitForTimeout(500); } catch {}
|
|
}
|
|
|
|
// Navigate to observer detail page
|
|
try {
|
|
const obsLink = await page.$('a[href*="/observers/"]');
|
|
if (obsLink) {
|
|
await obsLink.click();
|
|
await page.waitForTimeout(2000);
|
|
// Change days select
|
|
await cycleSelect('#obsDaysSelect');
|
|
}
|
|
} catch {}
|
|
|
|
// ══════════════════════════════════════════════
|
|
// PERF PAGE
|
|
// ══════════════════════════════════════════════
|
|
console.log(' [coverage] Perf page...');
|
|
await page.goto(`${BASE}/#/perf`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(2000);
|
|
await safeClick('#perfRefresh');
|
|
await page.waitForTimeout(1000);
|
|
await safeClick('#perfReset');
|
|
await page.waitForTimeout(500);
|
|
|
|
// ══════════════════════════════════════════════
|
|
// APP.JS — Router, theme, global features
|
|
// ══════════════════════════════════════════════
|
|
console.log(' [coverage] App.js — router + global...');
|
|
|
|
// Navigate to bad route to trigger error/404
|
|
await page.goto(`${BASE}/#/nonexistent-route`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Navigate to every route via hash
|
|
const allRoutes = ['home', 'nodes', 'packets', 'map', 'live', 'channels', 'traces', 'observers', 'analytics', 'perf'];
|
|
for (const route of allRoutes) {
|
|
try {
|
|
await page.evaluate((r) => { location.hash = '#/' + r; }, route);
|
|
await page.waitForTimeout(800);
|
|
} catch {}
|
|
}
|
|
|
|
// Trigger hashchange manually
|
|
try {
|
|
await page.evaluate(() => {
|
|
window.dispatchEvent(new HashChangeEvent('hashchange'));
|
|
});
|
|
await page.waitForTimeout(500);
|
|
} catch {}
|
|
|
|
// Theme toggle multiple times
|
|
for (let i = 0; i < 4; i++) {
|
|
await safeClick('#darkModeToggle');
|
|
await page.waitForTimeout(300);
|
|
}
|
|
|
|
// Dispatch theme-changed event
|
|
try {
|
|
await page.evaluate(() => {
|
|
window.dispatchEvent(new Event('theme-changed'));
|
|
});
|
|
await page.waitForTimeout(300);
|
|
} catch {}
|
|
|
|
// Hamburger menu
|
|
await safeClick('#hamburger');
|
|
await page.waitForTimeout(400);
|
|
// Click nav links in mobile menu
|
|
await clickAll('.nav-links .nav-link', 5);
|
|
await page.waitForTimeout(300);
|
|
|
|
// Favorites
|
|
await safeClick('#favToggle');
|
|
await page.waitForTimeout(500);
|
|
await clickAll('.fav-dd-item', 3);
|
|
// Click outside to close
|
|
try { await page.evaluate(() => document.body.click()); await page.waitForTimeout(300); } catch {}
|
|
await safeClick('#favToggle');
|
|
await page.waitForTimeout(300);
|
|
|
|
// Global search
|
|
await safeClick('#searchToggle');
|
|
await page.waitForTimeout(500);
|
|
await safeFill('#searchInput', 'test');
|
|
await page.waitForTimeout(1000);
|
|
// Click search result items
|
|
await clickAll('.search-result-item', 3);
|
|
await page.waitForTimeout(500);
|
|
// Close search
|
|
try { await page.keyboard.press('Escape'); } catch {}
|
|
await page.waitForTimeout(300);
|
|
|
|
// Ctrl+K shortcut
|
|
try {
|
|
await page.keyboard.press('Control+k');
|
|
await page.waitForTimeout(500);
|
|
await safeFill('#searchInput', 'node');
|
|
await page.waitForTimeout(800);
|
|
await page.keyboard.press('Escape');
|
|
await page.waitForTimeout(300);
|
|
} catch {}
|
|
|
|
// Click search overlay background to close
|
|
try {
|
|
await safeClick('#searchToggle');
|
|
await page.waitForTimeout(300);
|
|
await page.click('#searchOverlay', { position: { x: 5, y: 5 } });
|
|
await page.waitForTimeout(300);
|
|
} catch {}
|
|
|
|
// Navigate via nav links with data-route
|
|
for (const route of allRoutes) {
|
|
await safeClick(`a[data-route="${route}"]`);
|
|
await page.waitForTimeout(600);
|
|
}
|
|
|
|
// Exercise apiPerf console function
|
|
try {
|
|
await page.evaluate(() => { if (window.apiPerf) window.apiPerf(); });
|
|
await page.waitForTimeout(300);
|
|
} catch {}
|
|
|
|
// Exercise utility functions
|
|
try {
|
|
await page.evaluate(() => {
|
|
// timeAgo with various inputs
|
|
if (typeof timeAgo === 'function') {
|
|
timeAgo(null);
|
|
timeAgo(new Date().toISOString());
|
|
timeAgo(new Date(Date.now() - 30000).toISOString());
|
|
timeAgo(new Date(Date.now() - 3600000).toISOString());
|
|
timeAgo(new Date(Date.now() - 86400000 * 2).toISOString());
|
|
}
|
|
// truncate
|
|
if (typeof truncate === 'function') {
|
|
truncate('hello world', 5);
|
|
truncate(null, 5);
|
|
truncate('hi', 10);
|
|
}
|
|
// routeTypeName, payloadTypeName, payloadTypeColor
|
|
if (typeof routeTypeName === 'function') {
|
|
for (let i = 0; i <= 4; i++) routeTypeName(i);
|
|
}
|
|
if (typeof payloadTypeName === 'function') {
|
|
for (let i = 0; i <= 15; i++) payloadTypeName(i);
|
|
}
|
|
if (typeof payloadTypeColor === 'function') {
|
|
for (let i = 0; i <= 15; i++) payloadTypeColor(i);
|
|
}
|
|
// invalidateApiCache
|
|
if (typeof invalidateApiCache === 'function') {
|
|
invalidateApiCache();
|
|
invalidateApiCache('/test');
|
|
}
|
|
});
|
|
await page.waitForTimeout(300);
|
|
} catch {}
|
|
|
|
// ══════════════════════════════════════════════
|
|
// PACKET FILTER — exercise the filter parser
|
|
// ══════════════════════════════════════════════
|
|
console.log(' [coverage] Packet filter parser...');
|
|
try {
|
|
await page.evaluate(() => {
|
|
if (window.PacketFilter && window.PacketFilter.compile) {
|
|
const PF = window.PacketFilter;
|
|
// Valid expressions
|
|
const exprs = [
|
|
'type == ADVERT', 'type == GRP_TXT', 'type != ACK',
|
|
'snr > 0', 'snr < -5', 'snr >= 10', 'snr <= 3',
|
|
'hops > 1', 'hops == 0', 'rssi < -80',
|
|
'route == FLOOD', 'route == DIRECT', 'route == TRANSPORT_FLOOD',
|
|
'type == ADVERT && snr > 0', 'type == TXT_MSG || type == GRP_TXT',
|
|
'!type == ACK', 'NOT type == ADVERT',
|
|
'type == ADVERT && (snr > 0 || hops > 1)',
|
|
'observer == "test"', 'from == "abc"', 'to == "xyz"',
|
|
'has_text', 'is_encrypted',
|
|
'type contains ADV',
|
|
];
|
|
for (const e of exprs) {
|
|
try { PF.compile(e); } catch {}
|
|
}
|
|
// Bad expressions
|
|
const bad = ['@@@', '== ==', '(((', 'type ==', ''];
|
|
for (const e of bad) {
|
|
try { PF.compile(e); } catch {}
|
|
}
|
|
}
|
|
});
|
|
} catch {}
|
|
|
|
// ══════════════════════════════════════════════
|
|
// REGION FILTER — exercise
|
|
// ══════════════════════════════════════════════
|
|
console.log(' [coverage] Region filter...');
|
|
try {
|
|
// Open region filter on nodes page
|
|
await page.goto(`${BASE}/#/nodes`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(1500);
|
|
await safeClick('#nodesRegionFilter');
|
|
await page.waitForTimeout(300);
|
|
await clickAll('#nodesRegionFilter input[type="checkbox"]', 3);
|
|
await page.waitForTimeout(300);
|
|
} catch {}
|
|
|
|
// Region filter on packets
|
|
try {
|
|
await page.goto(`${BASE}/#/packets`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(1500);
|
|
await safeClick('#packetsRegionFilter');
|
|
await page.waitForTimeout(300);
|
|
await clickAll('#packetsRegionFilter input[type="checkbox"]', 3);
|
|
await page.waitForTimeout(300);
|
|
} catch {}
|
|
|
|
// ══════════════════════════════════════════════
|
|
// FINAL — navigate through all routes once more
|
|
// ══════════════════════════════════════════════
|
|
console.log(' [coverage] Final route sweep...');
|
|
for (const route of allRoutes) {
|
|
try {
|
|
await page.evaluate((r) => { location.hash = '#/' + r; }, route);
|
|
await page.waitForTimeout(500);
|
|
} catch {}
|
|
}
|
|
|
|
// 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); });
|