From 5777780fc8d0a6787b0fdbf299dfa119274528b4 Mon Sep 17 00:00:00 2001 From: Kpa-clawbot Date: Sun, 29 Mar 2026 19:46:01 -0700 Subject: [PATCH] refactor: parallel coverage collector (~30-60s vs 8min) (#272) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Redesigned frontend coverage collector with 7 parallel browser contexts. Coverage collector runs on master pushes only (skipped on PRs). ### Architecture 7 groups run simultaneously via `Promise.allSettled()`: - G1: Home + Customizer - G2: Nodes + Node Detail - G3: Packets + Packet Detail - G4: Map - G5: Analytics + Channels + Observers - G6: Live + Perf + Traces + Globals - G7: Utility functions (page.evaluate) ### Speed gains - `safeClick` 500ms → 100ms - `navHash` 150ms → 50ms - Removed redundant page visits and E2E-duplicate interactions - Wall time = slowest group (~30-60s estimated) ### 821 lines → ~450 lines Each group writes its own coverage JSON, nyc merges automatically. ### CI behavior - **PRs:** Coverage collector skipped (fast CI) - **Master:** Coverage collector runs (full synthetic user validation) Co-authored-by: you --- .github/workflows/deploy.yml | 9 +- .gitignore | 1 + scripts/collect-frontend-coverage.js | 1161 ++++++++++---------------- 3 files changed, 457 insertions(+), 714 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index efa5875..754afa6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -181,11 +181,10 @@ jobs: run: | BASE_URL=http://localhost:13581 node test-e2e-playwright.js 2>&1 | tee e2e-output.txt - # DISABLED: Coverage collector takes 8+ min. E2E tests extract window.__coverage__ directly. - # - name: Collect frontend coverage - # if: success() - # run: | - # BASE_URL=http://localhost:13581 node scripts/collect-frontend-coverage.js 2>&1 | tee fe-coverage-output.txt || true + - name: Collect frontend coverage (parallel) + if: success() && github.event_name == 'push' + run: | + BASE_URL=http://localhost:13581 node scripts/collect-frontend-coverage.js 2>&1 | tee fe-coverage-output.txt || true - name: Generate frontend coverage badges if: success() diff --git a/.gitignore b/.gitignore index 2a03560..363d23b 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ cmd/server/server.exe cmd/ingestor/ingestor.exe # CI trigger !test-fixtures/e2e-fixture.db +corescope-server diff --git a/scripts/collect-frontend-coverage.js b/scripts/collect-frontend-coverage.js index e10a8fb..01498e4 100644 --- a/scripts/collect-frontend-coverage.js +++ b/scripts/collect-frontend-coverage.js @@ -1,733 +1,477 @@ -// 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 +// Parallel frontend coverage collector +// Visits every page and exercises key UI interactions across 7 parallel browser contexts. +// Merges coverage JSONs at the end. Target: < 2 minutes. +// +// Skips interactions already covered by E2E tests (test-e2e-playwright.js): +// - Assertions on page load, data presence, column headers +// - Compare page (fully covered by E2E) +// - Audio Lab page (fully covered by E2E) +// - Map localStorage persistence, resize tests +// - Analytics sub-tab content assertions +// - Channels message click, Traces search, Observers health dots +// Focuses on: code paths E2E doesn't touch (customizer presets, filter expressions, +// VCR controls, node detail tabs, packet column toggles, utility functions, etc.) const { chromium } = require('playwright'); const fs = require('fs'); const path = require('path'); -async function collectCoverage() { +const BASE = process.env.BASE_URL || 'http://localhost:13581'; +const CLICK_TIMEOUT = 100; // ms — elements exist immediately or not at all +const NAV_WAIT = 50; // ms — SPA hash routing is instant + +async function run() { 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: navigate via hash (SPA — no full page reload needed after initial load) - async function navHash(hash, wait = 150) { - await page.evaluate((h) => { location.hash = h; }, hash); - await new Promise(r => setTimeout(r, wait)); + // Shared helpers factory — each group gets its own page + function helpers(page) { + page.setDefaultTimeout(5000); + const nav = async (hash) => { + await page.evaluate((h) => { location.hash = h; }, hash); + await page.waitForTimeout(NAV_WAIT); + }; + const click = async (sel) => { + try { await page.click(sel, { timeout: CLICK_TIMEOUT }); } catch {} + }; + const fill = async (sel, text) => { + try { await page.fill(sel, text); } catch {} + }; + const clickAll = async (sel, max = 10) => { + try { + const els = await page.$$(sel); + for (let i = 0; i < Math.min(els.length, max); i++) { + try { await els[i].click(); } catch {} + } + } catch {} + }; + const cycleSelect = async (sel) => { + try { + const opts = await page.$$eval(`${sel} option`, o => o.map(x => x.value)); + for (const v of opts) { try { await page.selectOption(sel, v); } catch {} } + } catch {} + }; + const init = async () => { + await page.goto(BASE, { waitUntil: 'domcontentloaded', timeout: 10000 }); + }; + return { nav, click, fill, clickAll, cycleSelect, init, page }; } - // Helper: safe click — 500ms timeout (elements exist immediately or not at all) - async function safeClick(selector, timeout) { - try { - await page.click(selector, { timeout: timeout || 500 }); - } catch {} - } + // ── Group definitions ────────────────────────────────────────────── - // Helper: safe fill - async function safeFill(selector, text) { - try { - await page.fill(selector, text); - } catch {} - } + // Group 1: Home + Customizer + async function group1() { + const ctx = await browser.newContext(); + const { nav, click, fill, clickAll, init, page } = helpers(await ctx.newPage()); + await init(); + console.log(' [cov] G1: Home + Customizer'); - // Helper: safe select - async function safeSelect(selector, value) { - try { - await page.selectOption(selector, value); - } catch {} - } + // Chooser flow + await page.evaluate(() => localStorage.clear()); + await nav('#/home'); + await click('#chooseNew'); + await fill('#homeSearch', 'test'); + await clickAll('.suggest-item', 3); + await clickAll('.suggest-claim', 2); + await fill('#homeSearch', ''); + await clickAll('.my-node-card', 3); + await clickAll('[data-action="health"]', 2); + await clickAll('[data-action="packets"]', 2); + await click('#toggleLevel'); + await clickAll('.faq-q, .question, [class*="accordion"]', 5); + await clickAll('.timeline-item', 5); + await clickAll('.health-claim', 2); + await clickAll('.card, .health-card', 3); + await clickAll('.mnc-remove', 2); - // 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(); } catch {} - } - } catch {} - } + // Experienced mode + await page.evaluate(() => localStorage.clear()); + await nav('#/home'); + await click('#chooseExp'); + await fill('#homeSearch', 'a'); + await clickAll('.suggest-item', 2); + await fill('#homeSearch', ''); + await page.evaluate(() => document.body.click()); - // 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); } catch {} - } - } catch {} - } - - // ══════════════════════════════════════════════ - // HOME PAGE - // ══════════════════════════════════════════════ - console.log(' [coverage] Home page — chooser...'); - // Clear localStorage to get chooser - await page.goto(BASE, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {}); - await page.evaluate(() => localStorage.clear()).catch(() => {}); - await page.goto(`${BASE}/#/home`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {}); - - // Click "I'm new" - await safeClick('#chooseNew'); - - // Now on home page as "new" user — interact with search - await safeFill('#homeSearch', 'test'); - // Click suggest items if any - await clickAll('.suggest-item', 3); - // Click suggest claim buttons - await clickAll('.suggest-claim', 2); - await safeFill('#homeSearch', ''); - - // Click my-node-card elements - await clickAll('.my-node-card', 3); - // Click health/packets buttons on cards - await clickAll('[data-action="health"]', 2); - await clickAll('[data-action="packets"]', 2); - - // Click toggle level - await safeClick('#toggleLevel'); - - // 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: 'domcontentloaded', timeout: 10000 }).catch(() => {}); - await safeClick('#chooseExp'); - - // Interact with experienced home page - await safeFill('#homeSearch', 'a'); - await clickAll('.suggest-item', 2); - await safeFill('#homeSearch', ''); - - // Click outside to dismiss suggest - await page.evaluate(() => document.body.click()).catch(() => {}); - - // ══════════════════════════════════════════════ - // NODES PAGE - // ══════════════════════════════════════════════ - console.log(' [coverage] Nodes page...'); - await navHash('#/nodes'); - - // Sort by EVERY column - for (const col of ['name', 'public_key', 'role', 'last_seen', 'advert_count']) { - try { await page.click(`th[data-sort="${col}"]`); } catch {} - // Click again for reverse sort - try { await page.click(`th[data-sort="${col}"]`); } catch {} - } - - // Click EVERY role tab - const roleTabs = await page.$$('.node-tab[data-tab]'); - for (const tab of roleTabs) { - try { await tab.click(); } catch {} - } - // Go back to "all" - try { await page.click('.node-tab[data-tab="all"]'); } catch {} - - // Click EVERY status filter - for (const status of ['active', 'stale', 'all']) { - try { await page.click(`#nodeStatusFilter .btn[data-status="${status}"]`); } catch {} - } - - // Cycle EVERY Last Heard option - await cycleSelect('#nodeLastHeard'); - - // Search - await safeFill('#nodeSearch', 'test'); - await safeFill('#nodeSearch', ''); - - // 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(); } catch {} - } - - // In side pane — click detail/analytics links - await safeClick('a[href*="/nodes/"]'); - // Click fav star - await clickAll('.fav-star', 2); - - // On node detail page — interact - // Click back button - await safeClick('#nodeBackBtn'); - - // 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 navHash('#/nodes/' + firstNodeKey); - - // 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}"]`); } catch {} - } + // Customizer + await click('#customizeToggle'); + for (const tab of ['branding', 'theme', 'nodes', 'home', 'export']) { + try { await page.click(`.cust-tab[data-tab="${tab}"]`, { timeout: CLICK_TIMEOUT }); } catch {} } - } catch {} + // Branding + try { + await page.click('.cust-tab[data-tab="branding"]', { timeout: CLICK_TIMEOUT }); + await fill('input[data-key="branding.siteName"]', 'Test'); + await fill('input[data-key="branding.tagline"]', 'Tag'); + } catch {} + // Theme presets + try { + await page.click('.cust-tab[data-tab="theme"]', { timeout: CLICK_TIMEOUT }); + await clickAll('.cust-preset-btn[data-preset]', 20); + const colorInputs = await page.$$('input[type="color"][data-theme]'); + for (let i = 0; i < Math.min(colorInputs.length, 3); i++) { + await colorInputs[i].evaluate(el => { el.value = '#ff5500'; el.dispatchEvent(new Event('input', { bubbles: true })); }); + } + } catch {} + await clickAll('[data-reset-theme]', 3); + await clickAll('[data-reset-node]', 3); + // Nodes tab colors + try { + await page.click('.cust-tab[data-tab="nodes"]', { timeout: CLICK_TIMEOUT }); + const nc = await page.$$('input[type="color"][data-node]'); + for (let i = 0; i < Math.min(nc.length, 3); i++) { + await nc[i].evaluate(el => { el.value = '#00ff00'; el.dispatchEvent(new Event('input', { bubbles: true })); }); + } + const tc = await page.$$('input[type="color"][data-type-color]'); + for (let i = 0; i < Math.min(tc.length, 3); i++) { + await tc[i].evaluate(el => { el.value = '#0000ff'; el.dispatchEvent(new Event('input', { bubbles: true })); }); + } + } catch {} + // Home tab + try { + await page.click('.cust-tab[data-tab="home"]', { timeout: CLICK_TIMEOUT }); + await fill('input[data-key="home.heroTitle"]', 'Hero'); + await clickAll('[data-move-step]', 2); + await clickAll('[data-rm-step]', 1); + await clickAll('[data-rm-check]', 1); + await clickAll('[data-rm-link]', 1); + } catch {} + // Export tab + try { + await page.click('.cust-tab[data-tab="export"]', { timeout: CLICK_TIMEOUT }); + await clickAll('.cust-panel[data-panel="export"] button', 3); + } catch {} + await click('#custResetPreview'); + await click('#custResetUser'); + await click('.cust-close'); - // 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 navHash('#/nodes/' + firstKey + '?scroll=paths'); + const cov = await page.evaluate(() => window.__coverage__); + await ctx.close(); + return cov; + } + + // Group 2: Nodes + Node Detail + async function group2() { + const ctx = await browser.newContext(); + const { nav, click, fill, clickAll, cycleSelect, init, page } = helpers(await ctx.newPage()); + await init(); + console.log(' [cov] G2: Nodes'); + + await nav('#/nodes'); + // Sort columns + for (const col of ['name', 'public_key', 'role', 'last_seen', 'advert_count']) { + try { await page.click(`th[data-sort="${col}"]`, { timeout: CLICK_TIMEOUT }); } catch {} + try { await page.click(`th[data-sort="${col}"]`, { timeout: CLICK_TIMEOUT }); } catch {} } - } catch {} + // Role tabs + await clickAll('.node-tab[data-tab]', 20); + try { await page.click('.node-tab[data-tab="all"]', { timeout: CLICK_TIMEOUT }); } catch {} + // Status filter + for (const s of ['active', 'stale', 'all']) { + try { await page.click(`#nodeStatusFilter .btn[data-status="${s}"]`, { timeout: CLICK_TIMEOUT }); } catch {} + } + await cycleSelect('#nodeLastHeard'); + await fill('#nodeSearch', 'test'); + await fill('#nodeSearch', ''); + // Click rows for side pane + const rows = await page.$$('#nodesBody tr'); + for (let i = 0; i < Math.min(rows.length, 3); i++) { + try { await rows[i].click(); } catch {} + } + await click('a[href*="/nodes/"]'); + await clickAll('.fav-star', 2); + await click('#nodeBackBtn'); - // ══════════════════════════════════════════════ - // PACKETS PAGE - // ══════════════════════════════════════════════ - console.log(' [coverage] Packets page...'); - await navHash('#/packets'); + // Node detail + try { + const key = await page.$eval('#nodesBody tr td:nth-child(2)', el => el.textContent.trim()); + if (key) { + await nav('#/nodes/' + key); + await clickAll('.tab-btn, [data-tab]', 10); + await click('#copyUrlBtn'); + await click('#showAllPaths'); + await click('#showAllFullPaths'); + for (const d of ['1', '7', '30', '365']) { + try { await page.click(`[data-days="${d}"]`, { timeout: CLICK_TIMEOUT }); } catch {} + } + await nav('#/nodes/' + key + '?scroll=paths'); + } + } catch {} - // Open filter bar - await safeClick('#filterToggleBtn'); + // Region filter + await nav('#/nodes'); + await click('#nodesRegionFilter'); + await clickAll('#nodesRegionFilter input[type="checkbox"]', 3); - // 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); + const cov = await page.evaluate(() => window.__coverage__); + await ctx.close(); + return cov; } - // Cycle ALL time window options - await cycleSelect('#fTimeWindow'); + // Group 3: Packets + Packet Detail + async function group3() { + const ctx = await browser.newContext(); + const { nav, click, fill, clickAll, cycleSelect, init, page } = helpers(await ctx.newPage()); + await init(); + console.log(' [cov] G3: Packets'); - // Toggle group by hash - await safeClick('#fGroup'); - await safeClick('#fGroup'); + await nav('#/packets'); + await click('#filterToggleBtn'); - // Toggle My Nodes filter - await safeClick('#fMyNodes'); - await safeClick('#fMyNodes'); + // Filter expressions (exercises packet-filter.js parser) + for (const expr of ['type == ADVERT', 'snr > 0', 'hops > 1', 'route == FLOOD', + 'snr > 5 && hops > 1', 'type == TXT_MSG || type == GRP_TXT', '!type == ACK', + 'type == ADVERT && (snr > 0 || hops > 1)', '@@@', '']) { + await fill('#packetFilterInput', expr); + } - // Click observer menu trigger - await safeClick('#observerTrigger'); - // Click items in observer menu - await clickAll('#observerMenu input[type="checkbox"]', 5); - await safeClick('#observerTrigger'); + await cycleSelect('#fTimeWindow'); + await click('#fGroup'); await click('#fGroup'); + await click('#fMyNodes'); await click('#fMyNodes'); - // Click type filter trigger - await safeClick('#typeTrigger'); - await clickAll('#typeMenu input[type="checkbox"]', 5); - await safeClick('#typeTrigger'); + // Observer/type menus + await click('#observerTrigger'); + await clickAll('#observerMenu input[type="checkbox"]', 5); + await click('#observerTrigger'); + await click('#typeTrigger'); + await clickAll('#typeMenu input[type="checkbox"]', 5); + await click('#typeTrigger'); - // Hash input - await safeFill('#fHash', 'abc123'); - await safeFill('#fHash', ''); + await fill('#fHash', 'abc123'); await fill('#fHash', ''); + await fill('#fNode', 'test'); + await clickAll('.node-filter-option', 3); + await fill('#fNode', ''); + await cycleSelect('#fObsSort'); - // Node filter - await safeFill('#fNode', 'test'); - await clickAll('.node-filter-option', 3); - await safeFill('#fNode', ''); + // Column toggle + await click('#colToggleBtn'); + await clickAll('#colToggleMenu input[type="checkbox"]', 8); + await click('#colToggleBtn'); - // Observer sort - await cycleSelect('#fObsSort'); + await click('#hexHashToggle'); await click('#hexHashToggle'); + await click('#pktPauseBtn'); await click('#pktPauseBtn'); - // Column toggle menu - await safeClick('#colToggleBtn'); - await clickAll('#colToggleMenu input[type="checkbox"]', 8); - await safeClick('#colToggleBtn'); + // Click packet rows + const rows = await page.$$('#pktBody tr'); + for (let i = 0; i < Math.min(rows.length, 3); i++) { + try { await rows[i].click(); } catch {} + } - // Hex hash toggle - await safeClick('#hexHashToggle'); - await safeClick('#hexHashToggle'); - - // Pause button - await safeClick('#pktPauseBtn'); - await safeClick('#pktPauseBtn'); - - // 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(); } catch {} - } - - // Resize handle drag simulation - try { + // Resize handle await page.evaluate(() => { - const handle = document.getElementById('pktResizeHandle'); - if (handle) { - handle.dispatchEvent(new MouseEvent('mousedown', { clientX: 500, bubbles: true })); + const h = document.getElementById('pktResizeHandle'); + if (h) { + h.dispatchEvent(new MouseEvent('mousedown', { clientX: 500, bubbles: true })); document.dispatchEvent(new MouseEvent('mousemove', { clientX: 400, bubbles: true })); document.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); } }); - } catch {} - // Click outside filter menus to close them - try { await page.evaluate(() => document.body.click()); - } catch {} + await nav('#/packets/deadbeef'); - // Navigate to specific packet by hash - await navHash('#/packets/deadbeef'); + // Region filter + await nav('#/packets'); + await click('#packetsRegionFilter'); + await clickAll('#packetsRegionFilter input[type="checkbox"]', 3); - // ══════════════════════════════════════════════ - // MAP PAGE - // ══════════════════════════════════════════════ - console.log(' [coverage] Map page...'); - await navHash('#/map'); - - // Toggle controls panel - await safeClick('#mapControlsToggle'); - - // Toggle each role checkbox on/off - try { - const roleChecks = await page.$$('#mcRoleChecks input[type="checkbox"]'); - for (const cb of roleChecks) { - try { await cb.click(); } catch {} - try { await cb.click(); } catch {} - } - } catch {} - - // Toggle clusters, heatmap, neighbors, hash labels - await safeClick('#mcClusters'); - await safeClick('#mcClusters'); - await safeClick('#mcHeatmap'); - await safeClick('#mcHeatmap'); - await safeClick('#mcNeighbors'); - await safeClick('#mcNeighbors'); - await safeClick('#mcHashLabels'); - await safeClick('#mcHashLabels'); - - // 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}"]`); } catch {} + const cov = await page.evaluate(() => window.__coverage__); + await ctx.close(); + return cov; } - // Click jump buttons (region jumps) - await clickAll('#mcJumps button', 5); + // Group 4: Map + async function group4() { + const ctx = await browser.newContext(); + const { nav, click, clickAll, cycleSelect, init, page } = helpers(await ctx.newPage()); + await init(); + console.log(' [cov] G4: Map'); - // Click markers - await clickAll('.leaflet-marker-icon', 5); - await clickAll('.leaflet-interactive', 3); + await nav('#/map'); + await click('#mapControlsToggle'); - // Click popups - await clickAll('.leaflet-popup-content a', 3); - - // Zoom controls - await safeClick('.leaflet-control-zoom-in'); - await safeClick('.leaflet-control-zoom-out'); - - // Toggle dark mode while on map (triggers tile layer swap) - await safeClick('#darkModeToggle'); - await safeClick('#darkModeToggle'); - - // ══════════════════════════════════════════════ - // ANALYTICS PAGE - // ══════════════════════════════════════════════ - console.log(' [coverage] Analytics page...'); - await navHash('#/analytics'); - - // Click EVERY analytics tab - const analyticsTabs = ['overview', 'rf', 'topology', 'channels', 'hashsizes', 'collisions', 'subpaths', 'nodes', 'distance']; - for (const tabName of analyticsTabs) { + // Role checkboxes toggle try { - await page.click(`#analyticsTabs [data-tab="${tabName}"]`, { timeout: 2000 }); + const cbs = await page.$$('#mcRoleChecks input[type="checkbox"]'); + for (const cb of cbs) { try { await cb.click(); await cb.click(); } catch {} } } catch {} + + await click('#mcClusters'); await click('#mcClusters'); + await click('#mcHeatmap'); await click('#mcHeatmap'); + await click('#mcNeighbors'); await click('#mcNeighbors'); + await click('#mcHashLabels'); await click('#mcHashLabels'); + await cycleSelect('#mcLastHeard'); + + for (const st of ['active', 'stale', 'all']) { + try { await page.click(`#mcStatusFilter .btn[data-status="${st}"]`, { timeout: CLICK_TIMEOUT }); } catch {} + } + await clickAll('#mcJumps button', 5); + await clickAll('.leaflet-marker-icon', 3); + await clickAll('.leaflet-interactive', 2); + await clickAll('.leaflet-popup-content a', 2); + await click('.leaflet-control-zoom-in'); + await click('.leaflet-control-zoom-out'); + + // Dark mode toggle triggers tile swap + await click('#darkModeToggle'); + await click('#darkModeToggle'); + + const cov = await page.evaluate(() => window.__coverage__); + await ctx.close(); + return cov; } - // On topology tab — click observer selector buttons - try { - await page.click('#analyticsTabs [data-tab="topology"]', { timeout: 2000 }); - await clickAll('#obsSelector .tab-btn', 5); - // Click the "All Observers" button - await safeClick('[data-obs="__all"]'); - } catch {} + // Group 5: Analytics + Channels + Observers + async function group5() { + const ctx = await browser.newContext(); + const { nav, click, clickAll, cycleSelect, init, page } = helpers(await ctx.newPage()); + await init(); + console.log(' [cov] G5: Analytics + Channels + Observers'); - // On collisions tab — click navigate rows - try { - await page.click('#analyticsTabs [data-tab="collisions"]', { timeout: 2000 }); - await clickAll('tr[data-action="navigate"]', 3); - } catch {} - - // On subpaths tab — click rows - try { - await page.click('#analyticsTabs [data-tab="subpaths"]', { timeout: 2000 }); - await clickAll('tr[data-action="navigate"]', 3); - } catch {} - - // On nodes tab — click sortable headers - try { - await page.click('#analyticsTabs [data-tab="nodes"]', { timeout: 2000 }); - await clickAll('.analytics-table th', 8); - } catch {} - - // Deep-link to each analytics tab via hash (avoid full page.goto) - for (const tab of analyticsTabs) { + await nav('#/analytics'); + const tabs = ['overview', 'rf', 'topology', 'channels', 'hashsizes', 'collisions', 'subpaths', 'nodes', 'distance']; + for (const t of tabs) { + try { await page.click(`#analyticsTabs [data-tab="${t}"]`, { timeout: 500 }); } catch {} + } + // Topology observer selector try { - await page.evaluate((t) => { location.hash = '#/analytics?tab=' + t; }, tab); - await new Promise(r => setTimeout(r, 100)); + await page.click('#analyticsTabs [data-tab="topology"]', { timeout: 500 }); + await clickAll('#obsSelector .tab-btn', 5); + await click('[data-obs="__all"]'); } catch {} + // Collisions navigate rows + try { + await page.click('#analyticsTabs [data-tab="collisions"]', { timeout: 500 }); + await clickAll('tr[data-action="navigate"]', 3); + } catch {} + // Nodes tab sort + try { + await page.click('#analyticsTabs [data-tab="nodes"]', { timeout: 500 }); + await clickAll('.analytics-table th', 8); + } catch {} + // Deep-link tabs + for (const t of tabs) { + await page.evaluate((tab) => { location.hash = '#/analytics?tab=' + tab; }, t); + await page.waitForTimeout(NAV_WAIT); + } + + // Channels + await nav('#/channels'); + await clickAll('.channel-item, .channel-row, .channel-card', 3); + await clickAll('table tbody tr', 3); + try { + const ch = await page.$eval('table tbody tr td:first-child', el => el.textContent.trim()); + if (ch) await nav('#/channels/' + ch); + } catch {} + + // Observers + await nav('#/observers'); + await clickAll('table tbody tr, .observer-card, .observer-row', 3); + try { + const link = await page.$('a[href*="/observers/"]'); + if (link) { + await link.click(); + await cycleSelect('#obsDaysSelect'); + } + } catch {} + + const cov = await page.evaluate(() => window.__coverage__); + await ctx.close(); + return cov; } - // Region filter on analytics - try { - await page.click('#analyticsRegionFilter'); - await clickAll('#analyticsRegionFilter input[type="checkbox"]', 3); - } catch {} + // Group 6: Live + Perf + Traces + App globals + async function group6() { + const ctx = await browser.newContext(); + const { nav, click, clickAll, init, page } = helpers(await ctx.newPage()); + await init(); + console.log(' [cov] G6: Live + Perf + Traces + globals'); - // ══════════════════════════════════════════════ - // CUSTOMIZE - // ══════════════════════════════════════════════ - console.log(' [coverage] Customizer...'); - await navHash('#/home'); - await safeClick('#customizeToggle'); - - // Click EVERY customizer tab - for (const tab of ['branding', 'theme', 'nodes', 'home', 'export']) { - try { await page.click(`.cust-tab[data-tab="${tab}"]`); } catch {} - } - - // On branding tab — change text inputs - try { - await page.click('.cust-tab[data-tab="branding"]'); - 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"]'); - const presets = await page.$$('.cust-preset-btn[data-preset]'); - for (const preset of presets) { - try { await preset.click(); } 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 })); - }); - } 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"]'); - 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 })); - }); - } 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 })); - }); - } catch {} - } - } catch {} - - // On home tab — edit home customization fields - try { - await page.click('.cust-tab[data-tab="home"]'); - 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); - } catch {} - } - } catch {} - - // On export tab - try { - await page.click('.cust-tab[data-tab="export"]'); - // 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 safeClick('#custResetUser'); - - // Close customizer - await safeClick('.cust-close'); - - // ══════════════════════════════════════════════ - // CHANNELS PAGE - // ══════════════════════════════════════════════ - console.log(' [coverage] Channels page...'); - await navHash('#/channels'); - // 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 navHash('#/channels/' + channelHash); - } - } catch {} - - // ══════════════════════════════════════════════ - // LIVE PAGE - // ══════════════════════════════════════════════ - console.log(' [coverage] Live page...'); - await navHash('#/live'); - - // VCR controls - await safeClick('#vcrPauseBtn'); - await safeClick('#vcrPauseBtn'); - - // VCR speed cycle - await safeClick('#vcrSpeedBtn'); - await safeClick('#vcrSpeedBtn'); - await safeClick('#vcrSpeedBtn'); - - // VCR mode / missed - await safeClick('#vcrMissed'); - - // VCR prompt buttons - await safeClick('#vcrPromptReplay'); - await safeClick('#vcrPromptSkip'); - - // Toggle visualization options - await safeClick('#liveHeatToggle'); - await safeClick('#liveHeatToggle'); - - await safeClick('#liveGhostToggle'); - await safeClick('#liveGhostToggle'); - - await safeClick('#liveRealisticToggle'); - await safeClick('#liveRealisticToggle'); - - await safeClick('#liveFavoritesToggle'); - await safeClick('#liveFavoritesToggle'); - - await safeClick('#liveMatrixToggle'); - await safeClick('#liveMatrixToggle'); - - await safeClick('#liveMatrixRainToggle'); - await safeClick('#liveMatrixRainToggle'); - - // Audio toggle and controls - await safeClick('#liveAudioToggle'); - try { - await page.fill('#audioBpmSlider', '120'); - // Dispatch input event on slider + // Live + await nav('#/live'); + await click('#vcrPauseBtn'); await click('#vcrPauseBtn'); + await click('#vcrSpeedBtn'); await click('#vcrSpeedBtn'); await click('#vcrSpeedBtn'); + await click('#vcrMissed'); + await click('#vcrPromptReplay'); await click('#vcrPromptSkip'); + await click('#liveHeatToggle'); await click('#liveHeatToggle'); + await click('#liveGhostToggle'); await click('#liveGhostToggle'); + await click('#liveRealisticToggle'); await click('#liveRealisticToggle'); + await click('#liveFavoritesToggle'); await click('#liveFavoritesToggle'); + await click('#liveMatrixToggle'); await click('#liveMatrixToggle'); + await click('#liveMatrixRainToggle'); await click('#liveMatrixRainToggle'); + await click('#liveAudioToggle'); await page.evaluate(() => { const s = document.getElementById('audioBpmSlider'); if (s) { s.value = '140'; s.dispatchEvent(new Event('input', { bubbles: true })); } - }); - } catch {} - await safeClick('#liveAudioToggle'); - - // VCR timeline click - try { + }).catch(() => {}); + await click('#liveAudioToggle'); + // VCR timeline click 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 - })); - } - }); - } catch {} + const c = document.getElementById('vcrTimeline'); + if (c) { const r = c.getBoundingClientRect(); c.dispatchEvent(new MouseEvent('click', { clientX: r.left + r.width * 0.5, clientY: r.top + r.height * 0.5, bubbles: true })); } + }).catch(() => {}); + await page.evaluate(() => window.dispatchEvent(new Event('resize'))).catch(() => {}); - // VCR LCD canvas - try { - await page.evaluate(() => { - const canvas = document.getElementById('vcrLcdCanvas'); - if (canvas) canvas.getContext('2d'); - }); - } catch {} + // Traces + await nav('#/traces'); + await clickAll('table tbody tr', 3); - // Resize the live page panel - try { - await page.evaluate(() => { - window.dispatchEvent(new Event('resize')); - }); - } catch {} + // Perf + await nav('#/perf'); + await click('#perfRefresh'); + await click('#perfReset'); - // ══════════════════════════════════════════════ - // TRACES PAGE - // ══════════════════════════════════════════════ - console.log(' [coverage] Traces page...'); - await navHash('#/traces'); - await clickAll('table tbody tr', 3); - - // ══════════════════════════════════════════════ - // OBSERVERS PAGE - // ══════════════════════════════════════════════ - console.log(' [coverage] Observers page...'); - await navHash('#/observers'); - // 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(); } catch {} - } - - // Navigate to observer detail page - try { - const obsLink = await page.$('a[href*="/observers/"]'); - if (obsLink) { - await obsLink.click(); - // Change days select - await cycleSelect('#obsDaysSelect'); + // App.js globals + await nav('#/nonexistent-route'); + for (const r of ['home', 'nodes', 'packets', 'map', 'live', 'channels', 'traces', 'observers', 'analytics', 'perf']) { + await page.evaluate((rt) => { location.hash = '#/' + rt; }, r); + await page.waitForTimeout(NAV_WAIT); } - } catch {} + await page.evaluate(() => window.dispatchEvent(new HashChangeEvent('hashchange'))).catch(() => {}); + for (let i = 0; i < 4; i++) await click('#darkModeToggle'); + await page.evaluate(() => window.dispatchEvent(new Event('theme-changed'))).catch(() => {}); - // ══════════════════════════════════════════════ - // PERF PAGE - // ══════════════════════════════════════════════ - console.log(' [coverage] Perf page...'); - await navHash('#/perf'); - await safeClick('#perfRefresh'); - await safeClick('#perfReset'); + // Hamburger + nav + await click('#hamburger'); + await clickAll('.nav-links .nav-link', 5); - // ══════════════════════════════════════════════ - // APP.JS — Router, theme, global features - // ══════════════════════════════════════════════ - console.log(' [coverage] App.js — router + global...'); + // Favorites + await click('#favToggle'); + await clickAll('.fav-dd-item', 3); + await page.evaluate(() => document.body.click()).catch(() => {}); + await click('#favToggle'); - // Navigate to bad route to trigger error/404 - await navHash('#/nonexistent-route'); - - // Navigate to every route via hash (50ms is enough for SPA hash routing) - const allRoutes = ['home', 'nodes', 'packets', 'map', 'live', 'channels', 'traces', 'observers', 'analytics', 'perf']; - for (const route of allRoutes) { + // Global search + await click('#searchToggle'); + try { await page.fill('#searchInput', 'test'); } catch {} + await clickAll('.search-result-item', 3); + try { await page.keyboard.press('Escape'); } catch {} try { - await page.evaluate((r) => { location.hash = '#/' + r; }, route); - await new Promise(r => setTimeout(r, 50)); + await page.keyboard.press('Control+k'); + await page.fill('#searchInput', 'node'); + await page.keyboard.press('Escape'); } catch {} + + // apiPerf + await page.evaluate(() => { if (window.apiPerf) window.apiPerf(); }).catch(() => {}); + + const cov = await page.evaluate(() => window.__coverage__); + await ctx.close(); + return cov; } - // Trigger hashchange manually - try { + // Group 7: Utility functions via page.evaluate() — no UI needed + async function group7() { + const ctx = await browser.newContext(); + const { init, page } = helpers(await ctx.newPage()); + await init(); + console.log(' [cov] G7: Utility functions'); + await page.evaluate(() => { - window.dispatchEvent(new HashChangeEvent('hashchange')); - }); - } catch {} - - // Theme toggle multiple times - for (let i = 0; i < 4; i++) { - await safeClick('#darkModeToggle'); - } - - // Dispatch theme-changed event - try { - await page.evaluate(() => { - window.dispatchEvent(new Event('theme-changed')); - }); - } catch {} - - // Hamburger menu - await safeClick('#hamburger'); - // Click nav links in mobile menu - await clickAll('.nav-links .nav-link', 5); - - // Favorites - await safeClick('#favToggle'); - await clickAll('.fav-dd-item', 3); - // Click outside to close - try { await page.evaluate(() => document.body.click()); } catch {} - await safeClick('#favToggle'); - - // Global search - await safeClick('#searchToggle'); - await safeFill('#searchInput', 'test'); - // Click search result items - await clickAll('.search-result-item', 3); - // Close search - try { await page.keyboard.press('Escape'); } catch {} - - // Ctrl+K shortcut - try { - await page.keyboard.press('Control+k'); - await safeFill('#searchInput', 'node'); - await page.keyboard.press('Escape'); - } catch {} - - // Click search overlay background to close - try { - await safeClick('#searchToggle'); - await page.click('#searchOverlay', { position: { x: 5, y: 5 } }); - } catch {} - - // Navigate via nav links with data-route - for (const route of allRoutes) { - await safeClick(`a[data-route="${route}"]`); - } - - // Exercise apiPerf console function - try { - await page.evaluate(() => { if (window.apiPerf) window.apiPerf(); }); - } catch {} - - // Exercise utility functions + packet filter parser in one evaluate call - console.log(' [coverage] Utility functions + packet filter...'); - try { - await page.evaluate(() => { - // Utility functions + // timeAgo if (typeof timeAgo === 'function') { timeAgo(null); timeAgo(new Date().toISOString()); @@ -736,9 +480,7 @@ async function collectCoverage() { timeAgo(new Date(Date.now() - 86400000 * 2).toISOString()); } if (typeof truncate === 'function') { - truncate('hello world', 5); - truncate(null, 5); - truncate('hi', 10); + truncate('hello world', 5); truncate(null, 5); truncate('hi', 10); } if (typeof routeTypeName === 'function') { for (let i = 0; i <= 4; i++) routeTypeName(i); @@ -750,11 +492,10 @@ async function collectCoverage() { for (let i = 0; i <= 15; i++) payloadTypeColor(i); } if (typeof invalidateApiCache === 'function') { - invalidateApiCache(); - invalidateApiCache('/test'); + invalidateApiCache(); invalidateApiCache('/test'); } - // Packet filter parser + // PacketFilter if (window.PacketFilter && window.PacketFilter.compile) { const PF = window.PacketFilter; const exprs = [ @@ -766,56 +507,58 @@ async function collectCoverage() { '!type == ACK', 'NOT type == ADVERT', 'type == ADVERT && (snr > 0 || hops > 1)', 'observer == "test"', 'from == "abc"', 'to == "xyz"', - 'has_text', 'is_encrypted', - 'type contains ADV', + 'has_text', 'is_encrypted', 'type contains ADV', ]; - for (const e of exprs) { - try { PF.compile(e); } catch {} - } - const bad = ['@@@', '== ==', '(((', 'type ==', '']; - for (const e of bad) { - try { PF.compile(e); } catch {} - } + for (const e of exprs) { try { PF.compile(e); } catch {} } + for (const e of ['@@@', '== ==', '(((', 'type ==', '']) { try { PF.compile(e); } catch {} } } }); - } catch {} - // ══════════════════════════════════════════════ - // REGION FILTER — exercise - // ══════════════════════════════════════════════ - console.log(' [coverage] Region filter...'); - try { - // Open region filter on nodes page (use hash nav, already visited) - await page.evaluate(() => { location.hash = '#/nodes'; }); - await new Promise(r => setTimeout(r, 100)); - await safeClick('#nodesRegionFilter'); - await clickAll('#nodesRegionFilter input[type="checkbox"]', 3); - } catch {} - - // Region filter on packets - try { - await page.evaluate(() => { location.hash = '#/packets'; }); - await new Promise(r => setTimeout(r, 100)); - await safeClick('#packetsRegionFilter'); - await clickAll('#packetsRegionFilter input[type="checkbox"]', 3); - } catch {} - - // ══════════════════════════════════════════════ - // FINAL — extract coverage (all routes already visited above) - // ══════════════════════════════════════════════ - - // 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'); + const cov = await page.evaluate(() => window.__coverage__); + await ctx.close(); + return cov; } + + // ── Run all groups in parallel ───────────────────────────────────── + console.log('Starting parallel coverage collection (7 groups)...'); + const start = Date.now(); + + const results = await Promise.allSettled([ + group1(), group2(), group3(), group4(), group5(), group6(), group7() + ]); + + const elapsed = ((Date.now() - start) / 1000).toFixed(1); + console.log(`All groups completed in ${elapsed}s`); + + // ── Merge coverage ───────────────────────────────────────────────── + const outDir = path.join(__dirname, '..', '.nyc_output'); + if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true }); + + let fileIndex = 0; + let totalFiles = 0; + for (let i = 0; i < results.length; i++) { + const r = results[i]; + if (r.status === 'fulfilled' && r.value) { + const fname = `frontend-coverage-g${i + 1}.json`; + fs.writeFileSync(path.join(outDir, fname), JSON.stringify(r.value)); + const count = Object.keys(r.value).length; + totalFiles = Math.max(totalFiles, count); + fileIndex++; + console.log(` Group ${i + 1}: ${count} instrumented files`); + } else if (r.status === 'rejected') { + console.log(` Group ${i + 1}: FAILED — ${r.reason?.message || r.reason}`); + } else { + console.log(` Group ${i + 1}: no coverage (not instrumented?)`); + } + } + + if (fileIndex === 0) { + console.log('WARNING: No __coverage__ found in any group — instrumentation may have failed'); + } else { + console.log(`Frontend coverage collected: ${fileIndex} groups, ${totalFiles} instrumented files`); + } + + await browser.close(); } -collectCoverage().catch(e => { console.error(e); process.exit(1); }); +run().catch(e => { console.error(e); process.exit(1); });