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