From 04f07bcbc50854a7be7ef9cba3e82ff50f70c50a Mon Sep 17 00:00:00 2001 From: you Date: Tue, 24 Mar 2026 04:57:09 +0000 Subject: [PATCH] coverage: massively expand frontend interaction coverage 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. --- scripts/collect-frontend-coverage.js | 1039 ++++++++++++++++++++------ 1 file changed, 796 insertions(+), 243 deletions(-) diff --git a/scripts/collect-frontend-coverage.js b/scripts/collect-frontend-coverage.js index 9cc13443..635562ea 100644 --- a/scripts/collect-frontend-coverage.js +++ b/scripts/collect-frontend-coverage.js @@ -22,7 +22,7 @@ async function collectCoverage() { async function safeClick(selector, timeout) { try { await page.click(selector, { timeout: timeout || 3000 }); - await page.waitForTimeout(400); + await page.waitForTimeout(300); } catch {} } @@ -30,7 +30,7 @@ async function collectCoverage() { async function safeFill(selector, text) { try { await page.fill(selector, text); - await page.waitForTimeout(400); + await page.waitForTimeout(300); } catch {} } @@ -38,374 +38,927 @@ async function collectCoverage() { async function safeSelect(selector, value) { try { await page.selectOption(selector, value); - await page.waitForTimeout(400); + await page.waitForTimeout(300); } catch {} } - // ── HOME PAGE ── - console.log(' [coverage] Home page...'); + // 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 onboarding buttons + + // Click "I'm new" await safeClick('#chooseNew'); - await page.waitForTimeout(800); - // Click FAQ items if present - const faqItems = await page.$$('.faq-q, .question, [class*="accordion"]').catch(() => []); - for (let i = 0; i < Math.min(faqItems.length, 3); i++) { - try { await faqItems[i].click(); await page.waitForTimeout(300); } catch {} - } - // Click cards - const cards = await page.$$('.card, .health-card, [class*="card"]').catch(() => []); - for (let i = 0; i < Math.min(cards.length, 3); i++) { - try { await cards[i].click(); await page.waitForTimeout(300); } catch {} - } - // Toggle level + await 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); - // Go back to home and choose experienced + + // 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); - // Click journey timeline items - const timelineItems = await page.$$('.timeline-item, [class*="journey"]').catch(() => []); - for (let i = 0; i < Math.min(timelineItems.length, 5); i++) { - try { await timelineItems[i].click(); await page.waitForTimeout(300); } catch {} - } - // ── NODES PAGE ── + // 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); - // Click column headers to sort - const headers = await page.$$('th'); - for (let i = 0; i < Math.min(headers.length, 6); i++) { - try { await headers[i].click(); await page.waitForTimeout(300); } catch {} + // 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 first header again for reverse sort - if (headers.length > 0) try { await headers[0].click(); await page.waitForTimeout(300); } catch {} - // Click sort headers by data-sort attribute - try { await page.click('th[data-sort="name"]'); await page.waitForTimeout(300); } catch {} - try { await page.click('th[data-sort="last_seen"]'); await page.waitForTimeout(300); } catch {} - - // Click role tabs using data-tab attribute + // Click EVERY role tab const roleTabs = await page.$$('.node-tab[data-tab]'); for (const tab of roleTabs) { - try { await tab.click(); await page.waitForTimeout(600); } catch {} + 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 {} } - // Click role tabs by data-role attribute - try { await page.click('[data-role="repeater"]'); await page.waitForTimeout(500); } catch {} - try { await page.click('[data-role="all"]'); await page.waitForTimeout(300); } catch {} + // Cycle EVERY Last Heard option + await cycleSelect('#nodeLastHeard'); - // Status filter buttons - const statusBtns = await page.$$('#nodeStatusFilter .btn, [data-status]'); - for (const btn of statusBtns) { - try { await btn.click(); await page.waitForTimeout(400); } catch {} - } - - // Click status filters by data-status attribute - try { await page.click('[data-status="active"]'); await page.waitForTimeout(300); } catch {} - try { await page.click('[data-status="all"]'); await page.waitForTimeout(300); } catch {} - - // Use search box + // Search await safeFill('#nodeSearch', 'test'); await page.waitForTimeout(500); await safeFill('#nodeSearch', ''); await page.waitForTimeout(300); - // Search by placeholder - try { await page.fill('input[placeholder*="Search"]', 'test'); await page.waitForTimeout(500); } catch {} - try { await page.fill('input[placeholder*="Search"]', ''); await page.waitForTimeout(300); } catch {}; - - // Use dropdowns (Last Heard, etc.) - const selects = await page.$$('select'); - for (const sel of selects) { - try { - const options = await sel.$$eval('option', opts => opts.map(o => o.value)); - if (options.length > 1) { - await sel.selectOption(options[1]); - await page.waitForTimeout(400); - if (options.length > 2) { - await sel.selectOption(options[2]); - await page.waitForTimeout(400); - } - await sel.selectOption(options[0]); - await page.waitForTimeout(300); - } - } catch {} - } - - // Click node rows to open side pane - const nodeRows = await page.$$('table tbody tr'); - for (let i = 0; i < Math.min(nodeRows.length, 3); i++) { + // 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 {} } - // Click Details link in side pane - await safeClick('a[href*="node/"]', 2000); + // 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); - // If on node detail page, interact with it + // On node detail page — interact + // Click back button + await safeClick('#nodeBackBtn'); + await page.waitForTimeout(500); + + // Navigate to a node detail page via hash try { - // Click tabs on detail page if any - const detailTabs = await page.$$('.tab-btn, [data-tab]'); - for (const tab of detailTabs) { - try { await tab.click(); await page.waitForTimeout(400); } catch {} + 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 {} - // Go back to nodes - await page.goto(`${BASE}/#/nodes`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {}); - await page.waitForTimeout(1500); + // 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 ── + // ══════════════════════════════════════════════ + // PACKETS PAGE + // ══════════════════════════════════════════════ console.log(' [coverage] Packets page...'); await page.goto(`${BASE}/#/packets`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {}); await page.waitForTimeout(2000); - // Type filter expressions - const filterInput = await page.$('#packetFilterInput'); - if (filterInput) { - const filters = ['type == ADVERT', 'type == GRP_TXT', 'hops > 1', 'type == GRP_TXT && hops > 1', 'rssi < -80', 'snr > 5', 'type == TXT_MSG', '']; - for (const f of filters) { - try { - await filterInput.fill(f); - await page.waitForTimeout(600); - } catch {} - } + // 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); } - // Direct filter interactions - try { await page.fill('#packetFilterInput', 'type == ADVERT'); await page.waitForTimeout(500); } catch {} - try { await page.fill('#packetFilterInput', ''); await page.waitForTimeout(300); } catch {} - // Change time window via select - try { await page.selectOption('#fTimeWindow', '60'); await page.waitForTimeout(500); } catch {} - // Click a packet row + // 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 { - const pktRows2 = await page.$$('table tbody tr'); - if (pktRows2.length) { await pktRows2[0].click(); await page.waitForTimeout(1000); } + 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 Group by Hash button - await safeClick('#fGroup'); - await page.waitForTimeout(800); + // Click outside filter menus to close them + try { + await page.evaluate(() => document.body.click()); + await page.waitForTimeout(300); + } catch {} - // Click packet rows (group headers) - const packetRows = await page.$$('table tbody tr'); - for (let i = 0; i < Math.min(packetRows.length, 5); i++) { - try { await packetRows[i].click(); await page.waitForTimeout(500); } catch {} - } + // Navigate to specific packet by hash + await page.goto(`${BASE}/#/packets/deadbeef`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {}); + await page.waitForTimeout(1500); - // Toggle group off - await safeClick('#fGroup'); - await page.waitForTimeout(800); - - // Change time window dropdown - const pktSelects = await page.$$('select'); - for (const sel of pktSelects) { - try { - const options = await sel.$$eval('option', opts => opts.map(o => ({ value: o.value, text: o.textContent }))); - const isTimeSelect = options.some(o => o.text.match(/hour|min|day|all|time/i)); - if (isTimeSelect) { - for (let i = 0; i < Math.min(options.length, 4); i++) { - await sel.selectOption(options[i].value); - await page.waitForTimeout(500); - } - } - } catch {} - } - - // ── MAP PAGE ── + // ══════════════════════════════════════════════ + // 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 - const markers = await page.$$('.leaflet-marker-icon, .leaflet-interactive'); - for (let i = 0; i < Math.min(markers.length, 3); i++) { - try { await markers[i].click(); await page.waitForTimeout(500); } catch {} - } + await clickAll('.leaflet-marker-icon', 5); + await clickAll('.leaflet-interactive', 3); - // Toggle filter checkboxes in legend - const checkboxes = await page.$$('input[type="checkbox"]'); - for (const cb of checkboxes) { - try { - await cb.click(); await page.waitForTimeout(300); - await cb.click(); await page.waitForTimeout(300); - } catch {} - } + // Click popups + await clickAll('.leaflet-popup-content a', 3); - // Toggle role filters by data-role - try { await page.click('input[data-role="companion"]'); await page.waitForTimeout(300); } catch {} - try { await page.click('input[data-role="companion"]'); await page.waitForTimeout(300); } catch {} - // Status filter on map - try { await page.click('[data-status="active"]'); await page.waitForTimeout(300); } catch {} - try { await page.click('[data-status="all"]'); await page.waitForTimeout(300); } catch {} + // 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 + // 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 ── + // ══════════════════════════════════════════════ + // ANALYTICS PAGE + // ══════════════════════════════════════════════ console.log(' [coverage] Analytics page...'); await page.goto(`${BASE}/#/analytics`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {}); - await page.waitForTimeout(3000); // wait for data to load + await page.waitForTimeout(3000); - // Click ALL analytics tabs using specific selector - const tabNames = ['overview', 'rf', 'topology', 'channels', 'hashsizes', 'collisions', 'subpaths', 'nodes', 'distance']; - for (const tabName of tabNames) { + // 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(1200); // give renderTab time + await page.waitForTimeout(1500); } catch {} } - // Click analytics tabs by data-tab directly (broader selector) - for (const tab of ['rf', 'topology', 'channels', 'hashsizes', 'collisions', 'subpaths', 'nodes', 'distance']) { - try { await page.click(`[data-tab="${tab}"]`); await page.waitForTimeout(800); } catch {} - } + // 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 {} - // Also test deep-link tabs - for (const tab of ['collisions', 'rf', 'distance', 'topology', 'nodes', 'subpaths']) { + // 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(2000); + await page.waitForTimeout(1500); } - // ── CUSTOMIZE ── + // 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 each customizer tab using specific selector - const custTabs = await page.$$('.cust-tab[data-tab]'); - for (const tab of custTabs) { - try { await tab.click(); await page.waitForTimeout(500); } catch {} + // Click 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 {} } - // Click preset theme buttons - const presets = await page.$$('.cust-preset-btn[data-preset]'); - for (let i = 0; i < Math.min(presets.length, 4); i++) { - try { await presets[i].click(); await page.waitForTimeout(400); } catch {} - } - - // Click presets by broader selector + // On branding tab — change text inputs try { - const presets2 = await page.$$('.cust-preset, [data-preset]'); - if (presets2.length) { await presets2[0].click(); await page.waitForTimeout(500); } + 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 {} - // Change a color input - const colorInputs = await page.$$('input[type="color"]'); - for (let i = 0; i < Math.min(colorInputs.length, 3); i++) { - try { - await colorInputs[i].evaluate(el => { el.value = '#ff0000'; el.dispatchEvent(new Event('input', {bubbles:true})); }); - await page.waitForTimeout(300); - } catch {} - } + // 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 {} - // Reset preview + // 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); - - // Reset user theme await safeClick('#custResetUser'); await page.waitForTimeout(400); // Close customizer - await safeClick('#customizeToggle'); + await safeClick('.cust-close'); + await page.waitForTimeout(300); - // Re-open customizer and use broader selectors - try { await page.click('#customizeToggle'); await page.waitForTimeout(500); } catch {} - for (const tab of ['branding', 'theme', 'nodes', 'home', 'export']) { - try { await page.click(`[data-tab="${tab}"]`); await page.waitForTimeout(300); } catch {} - } - try { await page.click('.cust-close'); await page.waitForTimeout(300); } catch {} - - // ── CHANNELS PAGE ── + // ══════════════════════════════════════════════ + // CHANNELS PAGE + // ══════════════════════════════════════════════ console.log(' [coverage] Channels page...'); await page.goto(`${BASE}/#/channels`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {}); await page.waitForTimeout(2000); - // Click any channel items - const channelItems = await page.$$('.channel-item, .channel-row, .channel-card, tr td'); - if (channelItems.length > 0) { - try { await channelItems[0].click(); await page.waitForTimeout(600); } catch {} - } + // Click channel rows/items + await clickAll('.channel-item, .channel-row, .channel-card', 3); + await clickAll('table tbody tr', 3); - // ── LIVE PAGE ── + // 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); - // Click live controls if any - const liveButtons = await page.$$('button'); - for (const btn of liveButtons) { - const text = await btn.textContent().catch(() => ''); - if (text.match(/pause|Pause|resume|Resume|clear|Clear|vcr|VCR/i)) { - try { await btn.click(); await page.waitForTimeout(400); } catch {} - } - } - // ── TRACES PAGE ── + // 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 ── + // ══════════════════════════════════════════════ + // OBSERVERS PAGE + // ══════════════════════════════════════════════ console.log(' [coverage] Observers page...'); await page.goto(`${BASE}/#/observers`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {}); await page.waitForTimeout(2000); // Click observer rows const obsRows = await page.$$('table tbody tr, .observer-card, .observer-row'); - for (let i = 0; i < Math.min(obsRows.length, 2); i++) { + for (let i = 0; i < Math.min(obsRows.length, 3); i++) { try { await obsRows[i].click(); await page.waitForTimeout(500); } catch {} } - // ── GLOBAL SEARCH ── - console.log(' [coverage] Global search...'); + // 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(800); - await safeFill('#searchInput', ''); - await page.keyboard.press('Escape').catch(() => {}); - await page.waitForTimeout(300); - - // ── FAVORITES ── - await safeClick('#favToggle'); - await page.waitForTimeout(400); - await safeClick('#favToggle'); - await page.waitForTimeout(300); - - // ── DARK MODE TOGGLE ── - await safeClick('#darkModeToggle'); + await page.waitForTimeout(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); - // Toggle via emoji button selector - try { - const themeBtn = await page.$('button:has-text("☀️"), button:has-text("🌙")'); - if (themeBtn) { await themeBtn.click(); await page.waitForTimeout(500); } - } catch {} - - // ── KEYBOARD SHORTCUT (Ctrl+K for search) ── + // 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 {} - // ── Navigate via nav links to exercise router ── - console.log(' [coverage] Nav link navigation...'); - const routes = ['home', 'packets', 'map', 'live', 'channels', 'nodes', 'traces', 'observers', 'analytics', 'perf']; - for (const route of routes) { + // 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(1000); + 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