// After Playwright tests, this script: // 1. Connects to the running test server // 2. Exercises frontend interactions to maximize code coverage // 3. Extracts window.__coverage__ from the browser // 4. Writes it to .nyc_output/ for merging const { chromium } = require('playwright'); const fs = require('fs'); const path = require('path'); async function collectCoverage() { const browser = await chromium.launch({ executablePath: process.env.CHROMIUM_PATH || undefined, args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'], headless: true }); const page = await browser.newPage(); page.setDefaultTimeout(10000); const BASE = process.env.BASE_URL || 'http://localhost:13581'; // Helper: safe click async function safeClick(selector, timeout) { try { await page.click(selector, { timeout: timeout || 3000 }); } catch {} } // Helper: safe fill async function safeFill(selector, text) { try { await page.fill(selector, text); } catch {} } // Helper: safe select async function safeSelect(selector, value) { try { await page.selectOption(selector, value); } catch {} } // Helper: click all matching elements async function clickAll(selector, max = 10) { try { const els = await page.$$(selector); for (let i = 0; i < Math.min(els.length, max); i++) { try { await els[i].click(); } 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); } 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(() => {}); // 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: 'networkidle', timeout: 15000 }).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 page.goto(`${BASE}/#/nodes`, { waitUntil: 'networkidle', timeout: 15000 }).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}"]`); } 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/"]', 2000); // 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 page.goto(`${BASE}/#/nodes/${firstNodeKey}`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {}); // 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 {} } } } catch {} // Node detail with scroll target try { const firstKey = await page.$eval('#nodesBody tr td:nth-child(2)', el => el.textContent.trim()).catch(() => null); if (firstKey) { await page.goto(`${BASE}/#/nodes/${firstKey}?scroll=paths`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {}); } } catch {} // ══════════════════════════════════════════════ // PACKETS PAGE // ══════════════════════════════════════════════ console.log(' [coverage] Packets page...'); await page.goto(`${BASE}/#/packets`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {}); // Open filter bar await safeClick('#filterToggleBtn'); // 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); } // Cycle ALL time window options await cycleSelect('#fTimeWindow'); // Toggle group by hash await safeClick('#fGroup'); await safeClick('#fGroup'); // Toggle My Nodes filter await safeClick('#fMyNodes'); await safeClick('#fMyNodes'); // Click observer menu trigger await safeClick('#observerTrigger'); // Click items in observer menu await clickAll('#observerMenu input[type="checkbox"]', 5); await safeClick('#observerTrigger'); // Click type filter trigger await safeClick('#typeTrigger'); await clickAll('#typeMenu input[type="checkbox"]', 5); await safeClick('#typeTrigger'); // Hash input await safeFill('#fHash', 'abc123'); await safeFill('#fHash', ''); // Node filter await safeFill('#fNode', 'test'); await clickAll('.node-filter-option', 3); await safeFill('#fNode', ''); // Observer sort await cycleSelect('#fObsSort'); // Column toggle menu await safeClick('#colToggleBtn'); await clickAll('#colToggleMenu input[type="checkbox"]', 8); await safeClick('#colToggleBtn'); // 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 { 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 })); } }); } catch {} // Click outside filter menus to close them try { await page.evaluate(() => document.body.click()); } catch {} // Navigate to specific packet by hash await page.goto(`${BASE}/#/packets/deadbeef`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {}); // ══════════════════════════════════════════════ // MAP PAGE // ══════════════════════════════════════════════ console.log(' [coverage] Map page...'); await page.goto(`${BASE}/#/map`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {}); // 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 {} } // Click jump buttons (region jumps) await clickAll('#mcJumps button', 5); // Click markers await clickAll('.leaflet-marker-icon', 5); await clickAll('.leaflet-interactive', 3); // Click popups await clickAll('.leaflet-popup-content a', 3); // Zoom controls await safeClick('.leaflet-control-zoom-in'); await 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 page.goto(`${BASE}/#/analytics`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {}); // 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 }); } catch {} } // 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 {} // 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 URL for (const tab of analyticsTabs) { await page.goto(`${BASE}/#/analytics?tab=${tab}`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {}); } // Region filter on analytics try { await page.click('#analyticsRegionFilter'); await clickAll('#analyticsRegionFilter input[type="checkbox"]', 3); } catch {} // ══════════════════════════════════════════════ // CUSTOMIZE // ══════════════════════════════════════════════ console.log(' [coverage] Customizer...'); await page.goto(BASE, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {}); 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 page.goto(`${BASE}/#/channels`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {}); // Click channel rows/items await clickAll('.channel-item, .channel-row, .channel-card', 3); await clickAll('table tbody tr', 3); // Navigate to a specific channel try { const channelHash = await page.$eval('table tbody tr td:first-child', el => el.textContent.trim()).catch(() => null); if (channelHash) { await page.goto(`${BASE}/#/channels/${channelHash}`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {}); } } catch {} // ══════════════════════════════════════════════ // LIVE PAGE // ══════════════════════════════════════════════ console.log(' [coverage] Live page...'); await page.goto(`${BASE}/#/live`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {}); // 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 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 { 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 {} // VCR LCD canvas try { await page.evaluate(() => { const canvas = document.getElementById('vcrLcdCanvas'); if (canvas) canvas.getContext('2d'); }); } catch {} // Resize the live page panel try { await page.evaluate(() => { window.dispatchEvent(new Event('resize')); }); } catch {} // ══════════════════════════════════════════════ // TRACES PAGE // ══════════════════════════════════════════════ console.log(' [coverage] Traces page...'); await page.goto(`${BASE}/#/traces`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {}); await clickAll('table tbody tr', 3); // ══════════════════════════════════════════════ // OBSERVERS PAGE // ══════════════════════════════════════════════ console.log(' [coverage] Observers page...'); await page.goto(`${BASE}/#/observers`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {}); // 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'); } } catch {} // ══════════════════════════════════════════════ // PERF PAGE // ══════════════════════════════════════════════ console.log(' [coverage] Perf page...'); await page.goto(`${BASE}/#/perf`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {}); await safeClick('#perfRefresh'); await safeClick('#perfReset'); // ══════════════════════════════════════════════ // 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(() => {}); // 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.waitForLoadState('networkidle').catch(() => {}); } catch {} } // Trigger hashchange manually try { 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 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'); } }); } 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 safeClick('#nodesRegionFilter'); await clickAll('#nodesRegionFilter input[type="checkbox"]', 3); } catch {} // Region filter on packets try { await page.goto(`${BASE}/#/packets`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {}); await safeClick('#packetsRegionFilter'); await clickAll('#packetsRegionFilter input[type="checkbox"]', 3); } 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.waitForLoadState('networkidle').catch(() => {}); } catch {} } // Extract coverage const coverage = await page.evaluate(() => window.__coverage__); await browser.close(); if (coverage) { const outDir = path.join(__dirname, '..', '.nyc_output'); if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true }); fs.writeFileSync(path.join(outDir, 'frontend-coverage.json'), JSON.stringify(coverage)); console.log('Frontend coverage collected: ' + Object.keys(coverage).length + ' files'); } else { console.log('WARNING: No __coverage__ object found — instrumentation may have failed'); } } collectCoverage().catch(e => { console.error(e); process.exit(1); });