mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-03 21:21:51 +00:00
c1ec9a043f
Add ~900 lines of deep branch-coverage interactions: - Utility functions with all edge cases (timeAgo, truncate, escapeHtml, formatHex, etc.) - roles.js: getHealthThresholds/getNodeStatus for all roles + edge inputs - PacketFilter: compile+match with mock packets, all operators, bad expressions - HopResolver/HopDisplay: init, resolve, renderPath with various inputs - RegionFilter: onChange, getSelected, isEnabled, setRegions, render - Customize: deep tab cycling, import/export, bad JSON, theme preview - WebSocket reconnection trigger - Keyboard shortcuts (Ctrl+K, Meta+K, Escape) - Window resize (mobile/tablet/desktop) for responsive branches - Error routes: nonexistent nodes/packets/observers/channels - localStorage corruption to trigger catch branches - Theme toggling (dark/light rapid switching, custom vars) - Live page: VCR modes, timeline clicks, speed cycling, all toggles - Audio Lab: play/stop/loop, BPM/volume sliders, voice selection - All analytics tabs via deep-link + sort headers - Packets: complex filter expressions, scroll-to-load, double-click - Nodes: special char search, all sort columns, fav stars - Channels: resize handle drag, theme observer, node tooltips - Observers/Observer Detail: sort, tabs, day cycling - Node Analytics: day buttons, tabs - Home: both new/experienced flows, empty search results - debouncedOnWS/onWS/offWS exercise
1873 lines
68 KiB
JavaScript
1873 lines
68 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 {}
|
|
|
|
// ══════════════════════════════════════════════
|
|
// DEEP BRANCH COVERAGE — page.evaluate() blitz
|
|
// ══════════════════════════════════════════════
|
|
console.log(' [coverage] Deep branch coverage via evaluate...');
|
|
|
|
// --- app.js utility functions with edge cases ---
|
|
try {
|
|
await page.goto(`${BASE}/#/nodes`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(1000);
|
|
await page.evaluate(() => {
|
|
// timeAgo edge cases — exercise every branch
|
|
if (typeof timeAgo === 'function') {
|
|
timeAgo(null);
|
|
timeAgo(undefined);
|
|
timeAgo('');
|
|
timeAgo('invalid-date');
|
|
timeAgo(new Date().toISOString()); // just now
|
|
timeAgo(new Date(Date.now() - 5000).toISOString()); // seconds
|
|
timeAgo(new Date(Date.now() - 30000).toISOString()); // 30s
|
|
timeAgo(new Date(Date.now() - 60000).toISOString()); // 1 min
|
|
timeAgo(new Date(Date.now() - 120000).toISOString()); // 2 min
|
|
timeAgo(new Date(Date.now() - 3600000).toISOString()); // 1 hour
|
|
timeAgo(new Date(Date.now() - 7200000).toISOString()); // 2 hours
|
|
timeAgo(new Date(Date.now() - 86400000).toISOString()); // 1 day
|
|
timeAgo(new Date(Date.now() - 172800000).toISOString()); // 2 days
|
|
timeAgo(new Date(Date.now() - 604800000).toISOString()); // 1 week
|
|
timeAgo(new Date(Date.now() - 2592000000).toISOString()); // 30 days
|
|
timeAgo(new Date(Date.now() - 31536000000).toISOString()); // 1 year
|
|
}
|
|
|
|
// truncate edge cases
|
|
if (typeof truncate === 'function') {
|
|
truncate('hello world', 5);
|
|
truncate('hi', 100);
|
|
truncate('', 5);
|
|
truncate(null, 5);
|
|
truncate(undefined, 5);
|
|
truncate('exactly5', 8);
|
|
}
|
|
|
|
// escapeHtml edge cases
|
|
if (typeof escapeHtml === 'function') {
|
|
escapeHtml('<script>alert("xss")</script>');
|
|
escapeHtml('& "quotes" <brackets>');
|
|
escapeHtml(null);
|
|
escapeHtml(undefined);
|
|
escapeHtml('');
|
|
escapeHtml('normal text');
|
|
}
|
|
|
|
// routeTypeName / payloadTypeName / payloadTypeColor — all values + unknown
|
|
if (typeof routeTypeName === 'function') {
|
|
for (let i = -1; i <= 10; i++) routeTypeName(i);
|
|
routeTypeName(undefined);
|
|
routeTypeName(null);
|
|
routeTypeName(999);
|
|
}
|
|
if (typeof payloadTypeName === 'function') {
|
|
for (let i = -1; i <= 20; i++) payloadTypeName(i);
|
|
payloadTypeName(undefined);
|
|
payloadTypeName(null);
|
|
payloadTypeName(999);
|
|
}
|
|
if (typeof payloadTypeColor === 'function') {
|
|
for (let i = -1; i <= 20; i++) payloadTypeColor(i);
|
|
payloadTypeColor(undefined);
|
|
payloadTypeColor(null);
|
|
}
|
|
|
|
// formatHex
|
|
if (typeof formatHex === 'function') {
|
|
formatHex('48656c6c6f');
|
|
formatHex('');
|
|
formatHex(null);
|
|
formatHex('abcdef0123456789');
|
|
formatHex('zzzz'); // non-hex
|
|
}
|
|
|
|
// createColoredHexDump
|
|
if (typeof createColoredHexDump === 'function') {
|
|
createColoredHexDump('48656c6c6f20576f726c64', []);
|
|
createColoredHexDump('48656c6c6f', [{start: 0, end: 2, label: 'test', cls: 'hex-header'}]);
|
|
createColoredHexDump('', []);
|
|
createColoredHexDump(null, []);
|
|
}
|
|
|
|
// buildHexLegend
|
|
if (typeof buildHexLegend === 'function') {
|
|
buildHexLegend([{label: 'Header', cls: 'hex-header'}, {label: 'Payload', cls: 'hex-payload'}]);
|
|
buildHexLegend([]);
|
|
buildHexLegend(null);
|
|
}
|
|
|
|
// debounce
|
|
if (typeof debounce === 'function') {
|
|
const fn = debounce(() => {}, 100);
|
|
fn(); fn(); fn();
|
|
}
|
|
|
|
// invalidateApiCache
|
|
if (typeof invalidateApiCache === 'function') {
|
|
invalidateApiCache();
|
|
invalidateApiCache('/api/nodes');
|
|
invalidateApiCache('/api/packets');
|
|
invalidateApiCache('/nonexistent');
|
|
}
|
|
|
|
// apiPerf
|
|
if (typeof apiPerf === 'function' || window.apiPerf) {
|
|
window.apiPerf();
|
|
}
|
|
|
|
// Favorites functions
|
|
if (typeof getFavorites === 'function') {
|
|
getFavorites();
|
|
}
|
|
if (typeof isFavorite === 'function') {
|
|
isFavorite('abc123');
|
|
isFavorite('');
|
|
isFavorite(null);
|
|
}
|
|
if (typeof toggleFavorite === 'function') {
|
|
toggleFavorite('test-pubkey-coverage-1');
|
|
toggleFavorite('test-pubkey-coverage-1'); // toggle off
|
|
toggleFavorite('test-pubkey-coverage-2');
|
|
}
|
|
if (typeof favStar === 'function') {
|
|
favStar('abc123', '');
|
|
favStar('abc123', 'extra-class');
|
|
favStar(null, '');
|
|
}
|
|
|
|
// syncBadgeColors
|
|
if (typeof syncBadgeColors === 'function') {
|
|
syncBadgeColors();
|
|
}
|
|
|
|
// getHealthThresholds — exercise both infra and non-infra
|
|
if (typeof getHealthThresholds === 'function') {
|
|
getHealthThresholds('repeater');
|
|
getHealthThresholds('room');
|
|
getHealthThresholds('companion');
|
|
getHealthThresholds('sensor');
|
|
getHealthThresholds('observer');
|
|
getHealthThresholds('unknown');
|
|
getHealthThresholds(null);
|
|
}
|
|
|
|
// getNodeStatus — exercise all branches
|
|
if (typeof getNodeStatus === 'function') {
|
|
getNodeStatus('repeater', Date.now());
|
|
getNodeStatus('repeater', Date.now() - 400000000); // stale
|
|
getNodeStatus('companion', Date.now());
|
|
getNodeStatus('companion', Date.now() - 100000000); // stale
|
|
getNodeStatus('room', Date.now());
|
|
getNodeStatus('sensor', Date.now());
|
|
getNodeStatus('observer', Date.now());
|
|
getNodeStatus('unknown', null);
|
|
getNodeStatus('repeater', undefined);
|
|
}
|
|
|
|
// getTileUrl
|
|
if (typeof getTileUrl === 'function') {
|
|
document.documentElement.setAttribute('data-theme', 'dark');
|
|
getTileUrl();
|
|
document.documentElement.setAttribute('data-theme', 'light');
|
|
getTileUrl();
|
|
}
|
|
});
|
|
await page.waitForTimeout(300);
|
|
} catch (e) { console.log(' [coverage] evaluate utility error:', e.message); }
|
|
|
|
// --- roles.js deep exercise ---
|
|
try {
|
|
await page.evaluate(() => {
|
|
// ROLE_COLORS, TYPE_COLORS, ROLE_LABELS, ROLE_STYLE, ROLE_EMOJI, ROLE_SORT
|
|
// Access all to ensure coverage
|
|
var roles = ['repeater', 'companion', 'room', 'sensor', 'observer', 'unknown'];
|
|
roles.forEach(function(r) {
|
|
var _ = window.ROLE_COLORS[r];
|
|
_ = window.ROLE_LABELS[r];
|
|
_ = window.ROLE_STYLE[r];
|
|
_ = window.ROLE_EMOJI[r];
|
|
});
|
|
// TYPE_COLORS access
|
|
var types = ['ADVERT', 'GRP_TXT', 'TXT_MSG', 'ACK', 'REQUEST', 'RESPONSE', 'TRACE', 'PATH', 'ANON_REQ', 'UNKNOWN'];
|
|
types.forEach(function(t) { var _ = window.TYPE_COLORS[t]; });
|
|
});
|
|
} catch {}
|
|
|
|
// --- WebSocket reconnection ---
|
|
console.log(' [coverage] WebSocket reconnect...');
|
|
try {
|
|
await page.evaluate(() => {
|
|
// Trigger WS close to exercise reconnection logic
|
|
if (window._ws) {
|
|
window._ws.close();
|
|
}
|
|
// Also try direct ws variable if exposed
|
|
var wsList = document.querySelectorAll('[class*="connected"]');
|
|
// Simulate a WS message event
|
|
try {
|
|
var fakeMsg = new MessageEvent('message', { data: JSON.stringify({ type: 'packet', data: {} }) });
|
|
} catch {}
|
|
});
|
|
await page.waitForTimeout(3000); // Let reconnect happen
|
|
} catch {}
|
|
|
|
// --- Keyboard shortcuts ---
|
|
console.log(' [coverage] Keyboard shortcuts...');
|
|
try {
|
|
await page.keyboard.press('Escape');
|
|
await page.waitForTimeout(200);
|
|
await page.keyboard.press('Control+k');
|
|
await page.waitForTimeout(500);
|
|
await page.keyboard.press('Escape');
|
|
await page.waitForTimeout(200);
|
|
await page.keyboard.press('Meta+k');
|
|
await page.waitForTimeout(300);
|
|
await page.keyboard.press('Escape');
|
|
await page.waitForTimeout(200);
|
|
} catch {}
|
|
|
|
// --- Window resize for responsive branches ---
|
|
console.log(' [coverage] Window resize...');
|
|
try {
|
|
await page.setViewportSize({ width: 375, height: 667 }); // iPhone SE
|
|
await page.waitForTimeout(500);
|
|
await page.evaluate(() => window.dispatchEvent(new Event('resize')));
|
|
await page.waitForTimeout(300);
|
|
|
|
await page.setViewportSize({ width: 768, height: 1024 }); // iPad
|
|
await page.waitForTimeout(500);
|
|
await page.evaluate(() => window.dispatchEvent(new Event('resize')));
|
|
await page.waitForTimeout(300);
|
|
|
|
await page.setViewportSize({ width: 1920, height: 1080 }); // Desktop
|
|
await page.waitForTimeout(500);
|
|
await page.evaluate(() => window.dispatchEvent(new Event('resize')));
|
|
await page.waitForTimeout(300);
|
|
} catch {}
|
|
|
|
// --- Navigate to error/invalid routes ---
|
|
console.log(' [coverage] Error routes...');
|
|
try {
|
|
await page.goto(`${BASE}/#/nodes/nonexistent-pubkey-12345`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(1500);
|
|
await page.goto(`${BASE}/#/packets/nonexistent-hash-abc`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(1500);
|
|
await page.goto(`${BASE}/#/observers/nonexistent-obs-id`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(1500);
|
|
await page.goto(`${BASE}/#/channels/nonexistent-channel`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(1500);
|
|
// node-analytics with bad key
|
|
await page.goto(`${BASE}/#/nodes/fake-key-999/analytics`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(1500);
|
|
// packet detail standalone
|
|
await page.goto(`${BASE}/#/packet/fake-hash-123`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(1500);
|
|
// Totally unknown route
|
|
await page.goto(`${BASE}/#/this-does-not-exist`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(1000);
|
|
} catch {}
|
|
|
|
// --- HopResolver exercise ---
|
|
console.log(' [coverage] HopResolver...');
|
|
try {
|
|
await page.evaluate(() => {
|
|
if (window.HopResolver) {
|
|
var HR = window.HopResolver;
|
|
if (HR.ready) HR.ready();
|
|
if (HR.resolve) {
|
|
try { HR.resolve([], 0, 0, 0, 0, null); } catch {}
|
|
try { HR.resolve(['AB', 'CD', 'EF'], 37.3, -121.9, 37.4, -121.8, 'obs1'); } catch {}
|
|
try { HR.resolve(null, 0, 0, 0, 0, null); } catch {}
|
|
}
|
|
if (HR.init) {
|
|
try { HR.init([], {}); } catch {}
|
|
try { HR.init([{public_key: 'abc', name: 'Test', lat: 37.3, lon: -121.9, role: 'repeater'}], {}); } catch {}
|
|
}
|
|
}
|
|
});
|
|
} catch {}
|
|
|
|
// --- HopDisplay exercise ---
|
|
console.log(' [coverage] HopDisplay...');
|
|
try {
|
|
await page.evaluate(() => {
|
|
if (window.HopDisplay) {
|
|
var HD = window.HopDisplay;
|
|
if (HD.renderPath) {
|
|
try { HD.renderPath([], {}, {}); } catch {}
|
|
try { HD.renderPath(['AB', 'CD'], {AB: {name: 'Node1', conflicts: []}, CD: {name: 'Node2'}}, {}); } catch {}
|
|
try { HD.renderPath(['XX'], {XX: {name: 'N', conflicts: [{name: 'C1'}, {name: 'C2'}]}}, {}); } catch {}
|
|
}
|
|
if (HD.renderHop) {
|
|
try { HD.renderHop('AB', {name: 'TestNode', conflicts: []}, {}); } catch {}
|
|
try { HD.renderHop('XY', null, {}); } catch {}
|
|
try { HD.renderHop('ZZ', {name: 'Multi', conflicts: [{name: 'A'}, {name: 'B'}]}, {globalFallback: true}); } catch {}
|
|
}
|
|
}
|
|
});
|
|
} catch {}
|
|
|
|
// --- PacketFilter deep exercise ---
|
|
console.log(' [coverage] PacketFilter deep...');
|
|
try {
|
|
await page.evaluate(() => {
|
|
if (window.PacketFilter) {
|
|
var PF = window.PacketFilter;
|
|
// compile + match with mock packet data
|
|
var mockPkt = {
|
|
payload_type: 0, route_type: 0, snr: 5.5, rssi: -70,
|
|
hop_count: 2, packet_hash: 'abc123', from_name: 'Node1',
|
|
to_name: 'Node2', observer_id: 'obs1', decoded_text: 'hello world',
|
|
is_encrypted: false
|
|
};
|
|
var exprs = [
|
|
'type == ADVERT', 'type != ADVERT', 'type == GRP_TXT',
|
|
'snr > 0', 'snr < 0', 'snr >= 5.5', 'snr <= 5.5', 'snr == 5.5',
|
|
'hops > 1', 'hops == 2', 'hops < 3',
|
|
'rssi > -80', 'rssi < -60', 'rssi >= -70',
|
|
'route == FLOOD', 'route == DIRECT',
|
|
'from == "Node1"', 'to == "Node2"', 'observer == "obs1"',
|
|
'has_text', 'is_encrypted', '!is_encrypted',
|
|
'type == ADVERT && snr > 0', 'type == ADVERT || snr > 0',
|
|
'!(type == ADVERT)', 'NOT type == GRP_TXT',
|
|
'(type == ADVERT || type == GRP_TXT) && snr > 0',
|
|
'type contains ADV', 'from contains Node',
|
|
'hash == "abc123"', 'hash contains abc',
|
|
];
|
|
for (var i = 0; i < exprs.length; i++) {
|
|
try {
|
|
var fn = PF.compile(exprs[i]);
|
|
if (fn) fn(mockPkt);
|
|
} catch {}
|
|
}
|
|
// Bad expressions
|
|
var bad = ['', ' ', '@@@', '== ==', '(((', 'type ==', '))', 'type !! ADVERT', null];
|
|
for (var j = 0; j < bad.length; j++) {
|
|
try { PF.compile(bad[j]); } catch {}
|
|
}
|
|
|
|
// Test match with different packet types
|
|
for (var t = 0; t <= 15; t++) {
|
|
var p = Object.assign({}, mockPkt, {payload_type: t});
|
|
try {
|
|
var fn2 = PF.compile('type == ADVERT');
|
|
if (fn2) fn2(p);
|
|
} catch {}
|
|
}
|
|
}
|
|
});
|
|
} catch {}
|
|
|
|
// --- RegionFilter deep exercise ---
|
|
console.log(' [coverage] RegionFilter deep...');
|
|
try {
|
|
await page.evaluate(() => {
|
|
if (window.RegionFilter) {
|
|
var RF = window.RegionFilter;
|
|
if (RF.onChange) {
|
|
var unsub = RF.onChange(function() {});
|
|
if (typeof unsub === 'function') unsub();
|
|
}
|
|
if (RF.getSelected) RF.getSelected();
|
|
if (RF.isEnabled) RF.isEnabled();
|
|
if (RF.setRegions) {
|
|
try { RF.setRegions(['US-W', 'US-E', 'EU']); } catch {}
|
|
}
|
|
if (RF.render) {
|
|
try { RF.render(document.createElement('div')); } catch {}
|
|
}
|
|
}
|
|
});
|
|
} catch {}
|
|
|
|
// --- Customize deep exercise ---
|
|
console.log(' [coverage] Customize deep branches...');
|
|
try {
|
|
await page.goto(BASE, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(500);
|
|
await safeClick('#customizeToggle');
|
|
await page.waitForTimeout(800);
|
|
|
|
// Exercise export/import
|
|
await page.evaluate(() => {
|
|
// Try to call internal customize functions
|
|
// Trigger autoSave by changing theme vars
|
|
document.documentElement.style.setProperty('--bg-primary', '#111111');
|
|
document.documentElement.style.setProperty('--bg-secondary', '#222222');
|
|
document.documentElement.style.setProperty('--text-primary', '#ffffff');
|
|
|
|
// Trigger theme-changed to exercise reapplyUserThemeVars
|
|
window.dispatchEvent(new Event('theme-changed'));
|
|
});
|
|
await page.waitForTimeout(500);
|
|
|
|
// Click through ALL customizer tabs again and interact
|
|
for (const tab of ['branding', 'theme', 'nodes', 'home', 'export']) {
|
|
try { await page.click(`.cust-tab[data-tab="${tab}"]`); await page.waitForTimeout(300); } catch {}
|
|
}
|
|
|
|
// Try import with bad JSON
|
|
try {
|
|
await page.click('.cust-tab[data-tab="export"]');
|
|
await page.waitForTimeout(300);
|
|
const importArea = await page.$('textarea[data-import], #custImportArea, textarea');
|
|
if (importArea) {
|
|
await importArea.fill('{"theme":{"--bg-primary":"#ff0000"}}');
|
|
await page.waitForTimeout(200);
|
|
await safeClick('#custImportBtn, [data-action="import"], button:has-text("Import")');
|
|
await page.waitForTimeout(300);
|
|
// Bad JSON
|
|
await importArea.fill('not json at all {{{');
|
|
await page.waitForTimeout(200);
|
|
await safeClick('#custImportBtn, [data-action="import"], button:has-text("Import")');
|
|
await page.waitForTimeout(300);
|
|
}
|
|
} catch {}
|
|
|
|
// Reset all
|
|
await safeClick('#custResetPreview');
|
|
await page.waitForTimeout(300);
|
|
await safeClick('#custResetUser');
|
|
await page.waitForTimeout(300);
|
|
await safeClick('.cust-close');
|
|
await page.waitForTimeout(300);
|
|
} catch {}
|
|
|
|
// --- Channels deep exercise ---
|
|
console.log(' [coverage] Channels deep...');
|
|
try {
|
|
await page.goto(`${BASE}/#/channels`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Exercise channel-internal functions
|
|
await page.evaluate(() => {
|
|
// Trigger resize handle drag
|
|
var handle = document.querySelector('.ch-resize, #chResizeHandle, [class*="resize"]');
|
|
if (handle) {
|
|
handle.dispatchEvent(new MouseEvent('mousedown', { clientX: 300, bubbles: true }));
|
|
document.dispatchEvent(new MouseEvent('mousemove', { clientX: 200, bubbles: true }));
|
|
document.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
|
|
}
|
|
|
|
// Exercise theme observer on channels page
|
|
document.documentElement.setAttribute('data-theme', 'dark');
|
|
document.documentElement.setAttribute('data-theme', 'light');
|
|
});
|
|
await page.waitForTimeout(500);
|
|
|
|
// Click sidebar items to trigger node tooltips
|
|
await clickAll('.ch-sender, .msg-sender, [data-sender]', 3);
|
|
await page.waitForTimeout(300);
|
|
|
|
// Click back button in channel detail
|
|
await safeClick('.ch-back, #chBack, [data-action="back"]');
|
|
await page.waitForTimeout(300);
|
|
} catch {}
|
|
|
|
// --- Live page deep exercise ---
|
|
console.log(' [coverage] Live page deep branches...');
|
|
try {
|
|
await page.goto(`${BASE}/#/live`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(3000);
|
|
|
|
// Exercise VCR deeply
|
|
await page.evaluate(() => {
|
|
// Trigger resize
|
|
window.dispatchEvent(new Event('resize'));
|
|
|
|
// Theme switch on live page
|
|
document.documentElement.setAttribute('data-theme', 'dark');
|
|
document.documentElement.setAttribute('data-theme', 'light');
|
|
document.documentElement.setAttribute('data-theme', 'dark');
|
|
});
|
|
await page.waitForTimeout(500);
|
|
|
|
// Click VCR rewind button
|
|
await safeClick('#vcrRewindBtn');
|
|
await page.waitForTimeout(500);
|
|
|
|
// Timeline click at different positions
|
|
await page.evaluate(() => {
|
|
var canvas = document.getElementById('vcrTimeline');
|
|
if (canvas) {
|
|
var rect = canvas.getBoundingClientRect();
|
|
// Click at start
|
|
canvas.dispatchEvent(new MouseEvent('click', { clientX: rect.left + 5, clientY: rect.top + rect.height/2, bubbles: true }));
|
|
// Click at end
|
|
canvas.dispatchEvent(new MouseEvent('click', { clientX: rect.right - 5, clientY: rect.top + rect.height/2, bubbles: true }));
|
|
// Click in middle
|
|
canvas.dispatchEvent(new MouseEvent('click', { clientX: rect.left + rect.width * 0.3, clientY: rect.top + rect.height/2, bubbles: true }));
|
|
}
|
|
});
|
|
await page.waitForTimeout(500);
|
|
|
|
// Exercise all VCR speed values
|
|
for (let i = 0; i < 6; i++) {
|
|
await safeClick('#vcrSpeedBtn');
|
|
await page.waitForTimeout(200);
|
|
}
|
|
|
|
// Toggle every live option
|
|
for (const id of ['liveHeatToggle', 'liveGhostToggle', 'liveRealisticToggle', 'liveFavoritesToggle', 'liveMatrixToggle', 'liveMatrixRainToggle']) {
|
|
await safeClick(`#${id}`);
|
|
await page.waitForTimeout(200);
|
|
await safeClick(`#${id}`);
|
|
await page.waitForTimeout(200);
|
|
}
|
|
|
|
// VCR pause/unpause/resume cycle
|
|
await safeClick('#vcrPauseBtn');
|
|
await page.waitForTimeout(300);
|
|
await safeClick('#vcrPauseBtn');
|
|
await page.waitForTimeout(300);
|
|
|
|
// Simulate receiving packets while in different VCR modes
|
|
await page.evaluate(() => {
|
|
// Fake a WS message to trigger bufferPacket in different modes
|
|
if (window._ws || true) {
|
|
var fakePackets = [
|
|
{ type: 'packet', data: { packet_hash: 'fake1', payload_type: 0, route_type: 0, snr: 5, rssi: -70, hop_count: 1, from_short: 'AA', to_short: 'BB', observer_id: 'obs', ts: Date.now() } },
|
|
{ type: 'packet', data: { packet_hash: 'fake2', payload_type: 1, route_type: 1, snr: -3, rssi: -90, hop_count: 3, from_short: 'CC', to_short: 'DD', observer_id: 'obs', ts: Date.now() } },
|
|
];
|
|
// Dispatch as custom events in case WS listeners are registered
|
|
fakePackets.forEach(function(p) {
|
|
try {
|
|
window.dispatchEvent(new CustomEvent('ws-message', { detail: p }));
|
|
} catch {}
|
|
});
|
|
}
|
|
});
|
|
await page.waitForTimeout(500);
|
|
} catch {}
|
|
|
|
// --- Audio Lab exercise ---
|
|
console.log(' [coverage] Audio Lab...');
|
|
try {
|
|
await page.goto(`${BASE}/#/audio-lab`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Click various audio lab controls
|
|
await safeClick('#alabPlay');
|
|
await page.waitForTimeout(300);
|
|
await safeClick('#alabStop');
|
|
await page.waitForTimeout(300);
|
|
await safeClick('#alabLoop');
|
|
await page.waitForTimeout(300);
|
|
|
|
// Change BPM and volume sliders
|
|
await page.evaluate(() => {
|
|
var bpm = document.getElementById('alabBPM');
|
|
if (bpm) { bpm.value = '80'; bpm.dispatchEvent(new Event('input', { bubbles: true })); }
|
|
var vol = document.getElementById('alabVol');
|
|
if (vol) { vol.value = '0.3'; vol.dispatchEvent(new Event('input', { bubbles: true })); }
|
|
var voice = document.getElementById('alabVoice');
|
|
if (voice) { voice.value = voice.options[0]?.value || ''; voice.dispatchEvent(new Event('change', { bubbles: true })); }
|
|
});
|
|
await page.waitForTimeout(300);
|
|
|
|
// Click voice buttons
|
|
await clickAll('#alabVoices button, [data-voice]', 5);
|
|
await page.waitForTimeout(300);
|
|
|
|
// Click sidebar packets
|
|
await clickAll('#alabSidebar tr, .alab-pkt-row', 3);
|
|
await page.waitForTimeout(300);
|
|
|
|
// Click hex bytes
|
|
await clickAll('[id^="hexByte"]', 10);
|
|
await page.waitForTimeout(300);
|
|
} catch {}
|
|
|
|
// --- Traces page deep ---
|
|
console.log(' [coverage] Traces deep...');
|
|
try {
|
|
await page.goto(`${BASE}/#/traces`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(2000);
|
|
// Click sort headers
|
|
await clickAll('th[data-sort], th.sortable, table thead th', 6);
|
|
await page.waitForTimeout(300);
|
|
// Click trace rows
|
|
await clickAll('table tbody tr', 5);
|
|
await page.waitForTimeout(500);
|
|
// Click trace detail links
|
|
await clickAll('a[href*="trace"], a[href*="packet"]', 3);
|
|
await page.waitForTimeout(300);
|
|
} catch {}
|
|
|
|
// --- Observers page deep ---
|
|
console.log(' [coverage] Observers deep...');
|
|
try {
|
|
await page.goto(`${BASE}/#/observers`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(2000);
|
|
// Click observer cards/rows
|
|
await clickAll('table tbody tr, .observer-card', 3);
|
|
await page.waitForTimeout(300);
|
|
// Sort columns
|
|
await clickAll('th[data-sort], th.sortable, table thead th', 5);
|
|
await page.waitForTimeout(300);
|
|
// Navigate to observer detail
|
|
await clickAll('a[href*="observers/"]', 2);
|
|
await page.waitForTimeout(1500);
|
|
// Cycle days select on detail
|
|
await cycleSelect('#obsDaysSelect');
|
|
await page.waitForTimeout(300);
|
|
} catch {}
|
|
|
|
// --- Observer Detail deep ---
|
|
console.log(' [coverage] Observer detail...');
|
|
try {
|
|
await page.goto(`${BASE}/#/observers/test-obs`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(2000);
|
|
// Click tabs
|
|
await clickAll('[data-tab], .tab-btn', 5);
|
|
await page.waitForTimeout(300);
|
|
// Cycle day selects
|
|
await cycleSelect('#obsDaysSelect');
|
|
await cycleSelect('select[data-days]');
|
|
await page.waitForTimeout(300);
|
|
} catch {}
|
|
|
|
// --- Node Analytics deep ---
|
|
console.log(' [coverage] Node Analytics...');
|
|
try {
|
|
// Try getting a real node key first
|
|
await page.goto(`${BASE}/#/nodes`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(1500);
|
|
const nodeKey = await page.$eval('#nodesBody tr td:nth-child(2)', el => el.textContent.trim()).catch(() => 'fake-key');
|
|
await page.goto(`${BASE}/#/nodes/${nodeKey}/analytics`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(2000);
|
|
// Click day buttons
|
|
for (const days of ['1', '7', '30', '365']) {
|
|
try { await page.click(`[data-days="${days}"]`); await page.waitForTimeout(800); } catch {}
|
|
}
|
|
// Click tabs
|
|
await clickAll('[data-tab], .tab-btn', 5);
|
|
await page.waitForTimeout(300);
|
|
} catch {}
|
|
|
|
// --- Perf page deep ---
|
|
console.log(' [coverage] Perf deep...');
|
|
try {
|
|
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);
|
|
// Exercise apiPerf from perf page context
|
|
await page.evaluate(() => { if (window.apiPerf) window.apiPerf(); });
|
|
await page.waitForTimeout(300);
|
|
} catch {}
|
|
|
|
// --- localStorage corruption / edge cases ---
|
|
console.log(' [coverage] localStorage edge cases...');
|
|
try {
|
|
await page.evaluate(() => {
|
|
// Corrupt favorites to trigger catch branch
|
|
localStorage.setItem('meshcore-favorites', 'not-json');
|
|
if (typeof getFavorites === 'function') getFavorites();
|
|
|
|
// Corrupt user theme
|
|
localStorage.setItem('meshcore-user-theme', 'not-json');
|
|
window.dispatchEvent(new Event('theme-changed'));
|
|
|
|
// Clean up
|
|
localStorage.removeItem('meshcore-favorites');
|
|
localStorage.removeItem('meshcore-user-theme');
|
|
});
|
|
await page.waitForTimeout(500);
|
|
} catch {}
|
|
|
|
// --- DOMContentLoaded / theme edge cases ---
|
|
console.log(' [coverage] Theme edge cases...');
|
|
try {
|
|
await page.evaluate(() => {
|
|
// Exercise reapplyUserThemeVars with valid theme
|
|
localStorage.setItem('meshcore-user-theme', JSON.stringify({
|
|
'--bg-primary': '#1a1a2e',
|
|
'--bg-secondary': '#16213e',
|
|
'--text-primary': '#e94560'
|
|
}));
|
|
window.dispatchEvent(new Event('theme-changed'));
|
|
|
|
// Switch dark/light rapidly
|
|
for (var i = 0; i < 4; i++) {
|
|
document.documentElement.setAttribute('data-theme', i % 2 === 0 ? 'dark' : 'light');
|
|
window.dispatchEvent(new Event('theme-changed'));
|
|
}
|
|
|
|
// Clean up
|
|
localStorage.removeItem('meshcore-user-theme');
|
|
});
|
|
await page.waitForTimeout(500);
|
|
} catch {}
|
|
|
|
// --- Map deep exercise ---
|
|
console.log(' [coverage] Map deep branches...');
|
|
try {
|
|
await page.goto(`${BASE}/#/map`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(3000);
|
|
|
|
// Toggle dark mode on map to exercise tile swap
|
|
await page.evaluate(() => {
|
|
document.documentElement.setAttribute('data-theme', 'dark');
|
|
window.dispatchEvent(new Event('theme-changed'));
|
|
});
|
|
await page.waitForTimeout(500);
|
|
await page.evaluate(() => {
|
|
document.documentElement.setAttribute('data-theme', 'light');
|
|
window.dispatchEvent(new Event('theme-changed'));
|
|
});
|
|
await page.waitForTimeout(500);
|
|
|
|
// Zoom events
|
|
await page.evaluate(() => {
|
|
// Trigger map resize
|
|
window.dispatchEvent(new Event('resize'));
|
|
});
|
|
await page.waitForTimeout(300);
|
|
|
|
// Click legend items if present
|
|
await clickAll('.legend-item, .leaflet-legend-item', 5);
|
|
await page.waitForTimeout(300);
|
|
|
|
// Search on map page
|
|
await safeFill('#mapSearch', 'test');
|
|
await page.waitForTimeout(500);
|
|
await safeFill('#mapSearch', '');
|
|
await page.waitForTimeout(300);
|
|
} catch {}
|
|
|
|
// --- Analytics deep per-tab exercise ---
|
|
console.log(' [coverage] Analytics deep per-tab...');
|
|
try {
|
|
// Distance tab with interactions
|
|
await page.goto(`${BASE}/#/analytics?tab=distance`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(2000);
|
|
await clickAll('th[data-sort], th.sortable, table thead th', 5);
|
|
await page.waitForTimeout(300);
|
|
|
|
// RF tab
|
|
await page.goto(`${BASE}/#/analytics?tab=rf`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(2000);
|
|
await clickAll('th[data-sort], table thead th', 5);
|
|
await page.waitForTimeout(300);
|
|
|
|
// Channels analytics
|
|
await page.goto(`${BASE}/#/analytics?tab=channels`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(2000);
|
|
await clickAll('table tbody tr', 3);
|
|
await page.waitForTimeout(300);
|
|
|
|
// Hash sizes
|
|
await page.goto(`${BASE}/#/analytics?tab=hashsizes`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Overview
|
|
await page.goto(`${BASE}/#/analytics?tab=overview`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(2000);
|
|
} catch {}
|
|
|
|
// --- Packets page deep branches ---
|
|
console.log(' [coverage] Packets deep branches...');
|
|
try {
|
|
await page.goto(`${BASE}/#/packets`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Exercise all sort columns
|
|
await clickAll('#pktHead th', 10);
|
|
await page.waitForTimeout(300);
|
|
// Click again for reverse
|
|
await clickAll('#pktHead th', 10);
|
|
await page.waitForTimeout(300);
|
|
|
|
// Scroll to bottom to trigger lazy loading
|
|
await page.evaluate(() => {
|
|
var table = document.querySelector('#pktBody');
|
|
if (table) table.parentElement.scrollTop = table.parentElement.scrollHeight;
|
|
});
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Exercise filter with complex expressions
|
|
await safeFill('#packetFilterInput', 'type == ADVERT && (snr > 0 || hops > 1) && rssi < -50');
|
|
await page.waitForTimeout(500);
|
|
await safeFill('#packetFilterInput', 'from contains "Node" || to contains "Node"');
|
|
await page.waitForTimeout(500);
|
|
await safeFill('#packetFilterInput', '');
|
|
await page.waitForTimeout(300);
|
|
|
|
// Double-click packet row
|
|
try {
|
|
const firstRow = await page.$('#pktBody tr');
|
|
if (firstRow) {
|
|
await firstRow.dblclick();
|
|
await page.waitForTimeout(500);
|
|
}
|
|
} catch {}
|
|
} catch {}
|
|
|
|
// --- Nodes page deep branches ---
|
|
console.log(' [coverage] Nodes deep branches...');
|
|
try {
|
|
await page.goto(`${BASE}/#/nodes`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Exercise search with special characters
|
|
await safeFill('#nodeSearch', '<script>');
|
|
await page.waitForTimeout(300);
|
|
await safeFill('#nodeSearch', ' ');
|
|
await page.waitForTimeout(300);
|
|
await safeFill('#nodeSearch', 'aaaaaaaaaaaaaaaaaaaa');
|
|
await page.waitForTimeout(300);
|
|
await safeFill('#nodeSearch', '');
|
|
await page.waitForTimeout(300);
|
|
|
|
// Click ALL sortable headers
|
|
await clickAll('#nodesHead th, th[data-sort]', 10);
|
|
await page.waitForTimeout(300);
|
|
|
|
// Exercise fav star on nodes page
|
|
await clickAll('.fav-star', 3);
|
|
await page.waitForTimeout(300);
|
|
|
|
// Click copy buttons
|
|
await clickAll('[data-copy], .copy-btn, #copyUrlBtn', 3);
|
|
await page.waitForTimeout(300);
|
|
} catch {}
|
|
|
|
// --- debouncedOnWS exercise ---
|
|
try {
|
|
await page.evaluate(() => {
|
|
if (typeof debouncedOnWS === 'function') {
|
|
var handler = debouncedOnWS(function(msg) {}, 100);
|
|
// offWS the handler
|
|
if (typeof offWS === 'function' && handler && handler.cancel) {
|
|
handler.cancel();
|
|
}
|
|
}
|
|
// onWS / offWS
|
|
if (typeof onWS === 'function' && typeof offWS === 'function') {
|
|
var fn = function() {};
|
|
onWS(fn);
|
|
offWS(fn);
|
|
}
|
|
});
|
|
} catch {}
|
|
|
|
// --- Home page deep branches ---
|
|
console.log(' [coverage] Home page deep...');
|
|
try {
|
|
// Test new user flow thoroughly
|
|
await page.evaluate(() => localStorage.clear());
|
|
await page.goto(`${BASE}/#/home`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(1500);
|
|
await safeClick('#chooseNew');
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Search with no results
|
|
await safeFill('#homeSearch', 'zzzznonexistent999');
|
|
await page.waitForTimeout(600);
|
|
// Clear
|
|
await safeFill('#homeSearch', '');
|
|
await page.waitForTimeout(300);
|
|
|
|
// Click FAQ items more aggressively
|
|
await clickAll('.faq-q', 10);
|
|
await page.waitForTimeout(300);
|
|
|
|
// Click timeline items
|
|
await clickAll('.tl-dot, .timeline-dot', 5);
|
|
await page.waitForTimeout(300);
|
|
|
|
// Switch modes
|
|
await page.evaluate(() => localStorage.clear());
|
|
await page.goto(`${BASE}/#/home`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
|
await page.waitForTimeout(1000);
|
|
await safeClick('#chooseExp');
|
|
await page.waitForTimeout(1000);
|
|
} catch {}
|
|
|
|
// --- Final theme cleanup ---
|
|
try {
|
|
await page.evaluate(() => {
|
|
document.documentElement.setAttribute('data-theme', 'light');
|
|
localStorage.removeItem('meshcore-user-theme');
|
|
localStorage.removeItem('meshcore-favorites');
|
|
});
|
|
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); });
|