diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 684fd64c..c8cf21a3 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -426,6 +426,26 @@ jobs: CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-channels-ws-race-1498-e2e.js 2>&1 | tee -a e2e-output.txt CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1487-byop-modal-layout-e2e.js 2>&1 | tee -a e2e-output.txt + # #1616: slide-over focus-restore flake-gate. Runs the slide-over + # E2E 20 consecutive times against the SAME backend instance so + # the Chromium-headless focus race documented in #1172/#1616 has + # a 20× shot at firing. Any single non-zero exit aborts. This is + # the architectural-fix gate — if it ever turns red post-merge, + # the focused-but-hidden state has crept back in. + # + # PERMANENT step. Adds ~3-4 min to the e2e-test job in exchange + # for closing out a flake family that was blocking ~8 unrelated + # PRs at a time. If profiling pressures the budget later, drop + # repeat count first; do not delete. + - name: Slide-over E2E flake-gate (#1616, --repeat-each=20) + run: | + set -e + for i in $(seq 1 20); do + echo "--- slide-over E2E run $i/20 ---" + BASE_URL=http://localhost:13581 node test-slideover-1056-e2e.js 2>&1 | tee -a slideover-repeat-output.txt + done + echo "20 passed" + - name: Collect frontend coverage (parallel) if: success() && github.event_name == 'push' run: | diff --git a/public/live.css b/public/live.css index 99fab451..6e033e5b 100644 --- a/public/live.css +++ b/public/live.css @@ -322,6 +322,18 @@ visibility: hidden; } +/* #1616 — the legend is informational (color swatches + labels). Its only + * interactive child is the corner-reposition button in .panel-header. The + * legend's bounding box overlaps the Leaflet topright Settings cog + * (#liveControlsToggle) on viewports where both pin to the right edge, + * which made Playwright (and real users on touch) miss the cog. Disable + * pointer events on the legend surface and re-enable only on the move + * button so the cog stays clickable through the legend's empty area. + */ +.live-legend { pointer-events: none; } +.live-legend .panel-header, +.live-legend .panel-corner-btn { pointer-events: auto; } + .legend-title { font-size: 9px; font-weight: 700; diff --git a/public/nodes.js b/public/nodes.js index b443e181..548184f1 100644 --- a/public/nodes.js +++ b/public/nodes.js @@ -1393,6 +1393,16 @@ }); const dupMap = buildDupNameMap(_allNodes); + // #1616 followup: capture the focused row's data-key BEFORE we replace + // the tbody — async events (`theme-refresh`, ws node updates, etc.) + // can fire renderRows() while the user is keyboard-navigating. Without + // restoration, focus drops to
on every re-render and the + // slide-over close-path focus-restore loses the race. + const tbodyHadFocus = tbody.contains(document.activeElement); + const focusedKey = tbodyHadFocus + ? (document.activeElement.closest('tr[data-key]') || {}).getAttribute && + document.activeElement.closest('tr[data-key]').getAttribute('data-key') + : null; tbody.innerHTML = sorted.map(n => { const roleColor = ROLE_COLORS[n.role] || '#6b7280'; const isClaimed = myKeys.has(n.public_key); @@ -1418,6 +1428,17 @@ var _ndTbl = document.getElementById('nodesTable'); if (_ndTbl) window.TableResponsive.register(_ndTbl); } + // #1616 followup: re-land focus on the replacement