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