From 883c80b3fb56eab00cc3f47d95a3c0b314ff6d89 Mon Sep 17 00:00:00 2001 From: dborup Date: Mon, 5 Oct 2026 12:09:29 +0000 Subject: [PATCH 1/3] test: reproduce the Affinity Debug toggle and mobile group-row aria-expanded (#254) test-issue-254-affinity-debug-toggle.js renders the node page's Affinity Debug card from the real nodes.js and checks for a disclosure button (aria-expanded, aria-controls on the hidden body, caret-right collapsed, caret-down expanded) with no inline handler, and that the toggle and the delegated click handler open and close it. Red on master: the card is inline in loadFullNode, with a broken inline onclick. test-packets.js: under the 600 px mobile breakpoint of mobile-page-actions.js a multi-observation group row is select-hash without aria-expanded; above it, it stays the #189 toggle. 3 cases red on master. Co-Authored-By: Claude Opus 5.5 --- test-all.sh | 1 + test-issue-254-affinity-debug-toggle.js | 196 ++++++++++++++++++++++++ test-packets.js | 88 +++++++++++ 3 files changed, 285 insertions(+) create mode 100644 test-issue-254-affinity-debug-toggle.js diff --git a/test-all.sh b/test-all.sh index a505c5a17..9ce3bdbcc 100755 --- a/test-all.sh +++ b/test-all.sh @@ -57,6 +57,7 @@ run test-top-routes-overlay.js run test-important-links-byte-filter.js run test-url-state.js run test-node-adverts.js +run test-issue-254-affinity-debug-toggle.js run test-perf-go-runtime.js run test-channel-psk-ux.js run test-channel-sidebar-layout.js diff --git a/test-issue-254-affinity-debug-toggle.js b/test-issue-254-affinity-debug-toggle.js new file mode 100644 index 000000000..17eeb95e2 --- /dev/null +++ b/test-issue-254-affinity-debug-toggle.js @@ -0,0 +1,196 @@ +/** + * #254: the node page's Affinity Debug card (shown only with debugAffinity, e.g. + * localStorage meshcore-affinity-debug=true) folds its body in and out. + * + * Its heading used to carry an inline onclick whose value held the Phosphor + * sprite markup with unescaped double quotes, so the browser cut the attribute + * short and the handler was a syntax error: the body could never open. Its + * carets were also reversed against the #189 convention (collapsed caret-right, + * expanded caret-down). + * + * Tests the real nodes.js in a vm sandbox: + * - the card renders a disclosure + + `; + } + + function toggleAffinityDebug(btn) { + const expanded = btn.getAttribute('aria-expanded') !== 'true'; + btn.setAttribute('aria-expanded', String(expanded)); + const body = btn.ownerDocument.getElementById(btn.getAttribute('aria-controls')); + if (body) body.hidden = !expanded; + const icon = btn.querySelector('.toggle-icon'); + if (icon) icon.innerHTML = affinityDebugCaretHtml(expanded); + return expanded; + } + + function onFullBodyClick(e) { + const btn = e.target.closest && e.target.closest('.affinity-debug-toggle'); + if (btn) toggleAffinityDebug(btn); + } + let directNode = null; // set when navigating directly to #/nodes/:pubkey let regionChangeHandler = null; @@ -570,6 +602,9 @@ `; document.getElementById('nodeBackBtn').addEventListener('click', () => { location.hash = '#/nodes'; }); + // Wired once here: loadFullNode re-renders the body (theme-refresh) and + // would stack listeners. + document.getElementById('nodeFullBody').addEventListener('click', onFullBodyClick); loadFullNode(directNode); // Escape to go back to nodes list document.addEventListener('keydown', function nodesEsc(e) { @@ -916,12 +951,7 @@
Loading neighbors…
- + ${renderAffinityDebugCard()}

Paths Through This Node

@@ -2148,6 +2178,9 @@ window._nodesRenderNodeTimestampText = renderNodeTimestampText; window._nodesGetStatusInfo = getStatusInfo; window._nodesGetStatusTooltip = getStatusTooltip; + window._nodesRenderAffinityDebugCard = renderAffinityDebugCard; + window._nodesToggleAffinityDebug = toggleAffinityDebug; + window._nodesOnFullBodyClick = onFullBodyClick; // #862: Expose search filter logic for testing window._nodesMatchesSearch = function(node, query) { diff --git a/public/packets.js b/public/packets.js index a06d8028d..826c3cd65 100644 --- a/public/packets.js +++ b/public/packets.js @@ -2404,6 +2404,29 @@ } } + // #254: mobile-page-actions.js owns the mobile breakpoint and the #1461 #7 + // redirect of a group-row click to select-hash. Without it nothing redirects. + function groupRowSelectsOnActivate() { + return !!(window.MobilePageActions && window.MobilePageActions.isMobile()); + } + + // A group row's action and aria-expanded depend on that breakpoint, so a + // resize that crosses it (e.g. a phone rotating) re-renders the visible rows. + let _groupRowsSelect = false; + let _groupRowModeTimer = null; + function _onGroupRowModeResize() { + clearTimeout(_groupRowModeTimer); + _groupRowModeTimer = setTimeout(() => { + const selects = groupRowSelectsOnActivate(); + if (selects === _groupRowsSelect) return; + _groupRowsSelect = selects; + if (!_displayGrouped) return; + _lastVisibleStart = -1; + _lastVisibleEnd = -1; + renderVisibleRows(); + }, 150); + } + // Build HTML for a single grouped packet row function buildGroupRowHtml(p, entryIdx = -1) { const isExpanded = expandedHashes.has(p.hash); @@ -2440,8 +2463,12 @@ // channels.js, network-digest.js, analytics.js and route-view.js. The row that // toggles reports its state in aria-expanded; a single-observation row cannot // expand, so it has neither. + // #254: under the mobile breakpoint activating a group row selects it (#1461 + // #7, the expand column is hidden there), so the row is select-hash and + // claims no expanded state. + const _grpToggles = !isSingle && !groupRowSelectsOnActivate(); const _grpCaret = isSingle ? '' : ''; - let html = ` + let html = ` ${_grpCaret} ${groupRegion ? `${groupRegion}` : '—'} ${renderTimestampCell(p.latest)} @@ -4257,6 +4284,8 @@ _themeRefreshHandler = () => { if (typeof renderTableRows === 'function') renderTableRows(); }; window.addEventListener('theme-refresh', _themeRefreshHandler); window.addEventListener('storage', _onStorageChange); + _groupRowsSelect = groupRowSelectsOnActivate(); + window.addEventListener('resize', _onGroupRowModeResize); var result = init(app, routeParam); // Install channel color picker on packets table (M2, #271) if (window.ChannelColorPicker) window.ChannelColorPicker.installPacketsTable(); @@ -4265,6 +4294,8 @@ destroy: function() { if (_themeRefreshHandler) { window.removeEventListener('theme-refresh', _themeRefreshHandler); _themeRefreshHandler = null; } window.removeEventListener('storage', _onStorageChange); + window.removeEventListener('resize', _onGroupRowModeResize); + clearTimeout(_groupRowModeTimer); return destroy(); } }); diff --git a/public/style.css b/public/style.css index 6c2007117..1990f320d 100644 --- a/public/style.css +++ b/public/style.css @@ -2860,6 +2860,14 @@ button.ch-item.ch-item-encrypted .ch-badge { filter: grayscale(0.6); } margin-bottom: 12px; border: 1px solid var(--border); } .node-full-card h4 { margin: 0 0 10px; font-size: 14px; color: var(--text-muted); text-transform: uppercase; letter-spacing: .5px; } +/* #254: the Affinity Debug heading is a disclosure button. It keeps the h4 + * look and spans the heading, so the whole row toggles as before. */ +.node-full-card h4 .affinity-debug-toggle { + display: flex; align-items: center; gap: 6px; width: 100%; + margin: 0; padding: 0; border: 0; background: none; cursor: pointer; + font: inherit; color: inherit; letter-spacing: inherit; text-transform: inherit; text-align: start; +} +@media (pointer: coarse) { .node-full-card h4 .affinity-debug-toggle { min-height: 48px; } } .node-activity-list { display: flex; flex-direction: column; gap: 6px; } .node-activity-item { display: flex; gap: 8px; font-size: 13px; padding: 6px 0; diff --git a/test-issue-254-affinity-debug-toggle.js b/test-issue-254-affinity-debug-toggle.js index 17eeb95e2..7e388e0ae 100644 --- a/test-issue-254-affinity-debug-toggle.js +++ b/test-issue-254-affinity-debug-toggle.js @@ -82,7 +82,7 @@ function attr(tag, name) { return m ? (m[1] === undefined ? '' : m[1]) : null; } function caretsIn(html) { - return (html.match(/#ph-caret-[a-z]+/g) || []).map((s) => s.slice(1)); + return (html.match(/#ph-caret-[a-z]+/g) || []).map((s) => s.slice('#ph-'.length)); } // A minimal DOM for the rendered card: the toggle button, its .toggle-icon and