Skip to content

Fix GUI state bugs and add React hook test coverage (A-13, B-15..B-19, M-6, B-27) - #170

Merged
fstubner merged 2 commits into
mainfrom
fix/gui-state-and-a11y
Aug 14, 2026
Merged

fstubner merged 2 commits into
mainfrom
fix/gui-state-and-a11y

Conversation

@fstubner

Copy link
Copy Markdown
Owner

Why these survived

B-27: zero React hook or component tests against 87 modules. A-13, B-16 and B-17 all live in that gap. This adds @testing-library/react + jsdom and lands each fix with a test verified to fail against the old code — I reverted B-16 and confirmed the test goes red before restoring it.

jsdom is opted into per-file via @vitest-environment jsdom, so the ~57 existing pure-module tests keep running in node.

72 tests pass, up from 57.

Fixes

# Bug
A-13 Workspace search jumped via setActiveTabId + setTimeout(() => selectRow(i), 0). The timeout fired after commit but held the pre-switch closure, so it patched the previous tab and clamped against its row count. Replaced with selectRowInTab(tabId, index).
B-16 The selection-reset effect keyed on sortKey/sortDir. Each tool kind has its own DEFAULT_SORT, so merely switching tabs looked like a re-sort and wiped the destination tab's selection. Now compares the previous tab id — re-sorting the same tab still resets.
B-17 setActiveTabId called inside a setTabs updater. React requires updaters to be pure; StrictMode double-invokes them. Worked only because the call was idempotent.
B-18 Global keydown listener torn down and re-attached every render — deps included a fresh object and two fresh closures, and a 3-second stats poll re-renders App continuously. Handler now read through a ref; listener attaches once.
B-15 Progress subscription had no .catch, so a rejected listen() left every later operation silently reporting no progress.
B-19 "Copied" reported even when the write failed — .catch(() => undefined) swallowed the rejection and the optional chain made a missing navigator.clipboard a silent no-op. All four copy paths now route through one reporting helper.
M-6 An unterminated quote made the whole tail one literal token, so typing "open en route to "open" silently matched nothing.

Reproduced live before fixing

Driven through the running GUI at ?demo=screenshot:

  • M-6 — open→4 rows, 'open'→4, 'open→0
  • B-16 — Scan→ARP→Scan wiped a 3-row selection to 0; Scan→Scan→Scan kept 3
  • A-13 — parked on ARP, searched nginx (row idx 1); jump switched tabs but selected idx 0

Note on scope

useWorkspace crossed the file-size cap, so selection handling moved to useSelection.ts — the split the gate's own note already prescribed ("split selection handling next").

Accessibility (H-3, B-20/21/22, C-13) is deliberately not in this PR; it's the next one.

B-27 found zero React hook or component tests against 87 modules. A-13,
B-16 and B-17 all lived in that gap, which is why they survived. Adds
@testing-library/react + jsdom (opted into per-file, so the ~57 existing
pure tests keep running in node) and lands each fix with a test that was
verified to fail against the old code.

A-13: workspace search jumped to a row via setActiveTabId followed by
setTimeout(() => selectRow(i), 0). The timeout fired after commit but
still held the pre-switch closure, so it patched the *previous* tab and
clamped against that tab's row count. Reproduced live: parked on ARP,
searched nginx (row index 1), the jump switched tabs correctly but
selected index 0. Replaced with selectRowInTab(tabId, index), which
derives the destination tab's rows and has no ordering dependency.

B-16: the selection-reset effect keyed on sortKey/sortDir, and each tool
kind has its own DEFAULT_SORT, so merely switching tabs looked like a
re-sort and wiped the destination tab's selection. Reproduced live:
scan -> ARP -> scan dropped a 3-row selection to 0 while scan -> scan ->
scan kept all 3. Now compares the previous tab id so re-sorting the same
tab still resets, which is the behaviour that had to be preserved.

B-17: setActiveTabId was called from inside a setTabs updater. React
requires updaters to be pure and StrictMode double-invokes them in dev;
this worked only because the call happened to be idempotent. The
replacement id is now computed outside the updater.

B-18: the global keydown listener was torn down and re-attached on every
render -- deps included a fresh object and two fresh closures, and a
3-second stats poll re-renders App continuously. The handler is now read
through a ref and the listener attaches once.

B-15: the progress-event subscription had no .catch, so a rejected
listen() left every later operation silently reporting no progress. Every
other Tauri call in the codebase was already guarded.

B-19: "Copied" was reported even when the write failed -- .catch(() =>
undefined) swallowed the rejection and the optional chain made a missing
navigator.clipboard a silent no-op. All four copy paths now route through
one helper that reports failure, matching what export already did. The
transfer.ts helpers become pure text builders.

M-6: an unterminated quote made the whole tail one literal token, so
typing "open on the way to "open" silently matched nothing. Reproduced
live: open -> 4 rows, 'open' -> 4, 'open -> 0. The tokenizer now
re-tokenizes without the dangling quote, so it behaves as if the quote
had not been typed yet. A mid-word apostrophe (don't) still never opens
a quote.

useWorkspace crossed the file-size cap, so selection handling moves to
useSelection.ts -- the split the gate's own note prescribed.

72 tests pass, up from 57.
jsdom 30 requires Node ^22.22.2 || ^24.15.0 || >=26, and CI runs Node 20,
so it failed with `webidl.util.markAsUncloneable is not a function` from
undici. It only passed locally by luck -- this host is on 24.14.1, also
below jsdom 30's floor, and npm had warned EBADENGINE at install time.

jsdom 26 declares >=18. Verified by running the suite under Node 20
directly rather than trusting the range: 72 tests pass.
@fstubner
fstubner merged commit 617206c into main Aug 14, 2026
15 of 16 checks passed
@fstubner
fstubner deleted the fix/gui-state-and-a11y branch August 14, 2026 22:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant