Skip to content

fix(ui): guard channels scroll listener; 48 px gesture-hint and hidden-column pill (#314) - #331

Merged
dborup merged 2 commits into
masterfrom
codex/issue-314-chscroll-touch-targets
Oct 7, 2026
Merged

dborup merged 2 commits into
masterfrom
codex/issue-314-chscroll-touch-targets

Conversation

@dborup

@dborup dborup commented Oct 7, 2026

Copy link
Copy Markdown
Owner

Relates to #314

Two pre-existing UI defects from the browser check of v0.2.2-rc1. Neither is a regression; both are present in v0.2.1. Tests first: both go red on origin/master and green on this branch.

Plan

  1. Write the two tests against master and confirm they fail for the reason in the issue.
  2. Point 1 — make the #chMessages scroll handler survive the page teardown, and decide whether it is also a listener leak.
  3. Point 2 — put both controls in the existing @media (pointer: coarse) 48 px block, the accessibility: full mobile accessibility audit findings Kpa-clawbot/CoreScope#630/fix(css): raise the remaining 44 px controls to the 48 px touch-target minimum (#235) #239 pattern, and extend test-touch-targets.js.
  4. Measure both controls in a real browser at 375×812 with touch and on desktop, and navigate #/channels ↔ #/live repeatedly with the console open.

1. The channels scroll listener

public/channels.js registered an anonymous scroll listener on #chMessages inside init():

msgEl.addEventListener('scroll', () => {
  const atBottom = msgEl.scrollHeight - msgEl.scrollTop - msgEl.clientHeight < 60;
  autoScroll = atBottom;
  document.getElementById('chScrollBtn').classList.toggle('hidden', atBottom);
});

The router (public/app.js) destroys the current page and then lets the next page's init() rewrite #app, which detaches #chMessages. A scroll can still reach that detached element, and #chScrollBtn is no longer in the document by then, so the lookup returned null.

Is it also a leak? Yes, of the #259/#282 class, but scoped to the element rather than to document: each visit left one detached #chMessages holding a live handler, which keeps the old subtree reachable. So both halves are fixed:

  • the lookup is optional-chained, the way all four other #chScrollBtn call sites in the file already are, and the handler reads the pane off event.currentTarget so it does not care whether the pane is still in the document;
  • the handler is a stable module-level function, so a repeat addEventListener is a DOM no-op, init() drops the pane it held before taking the new one, and destroy() removes it. At most one live listener, whatever the call order.

document.getElementById('chScrollBtn').addEventListener('click', …) on the next line was the same unguarded shape and is optional-chained too.

2. Two mobile tap targets

Both join the existing @media (pointer: coarse) block in style.css (the Kpa-clawbot#630 group that #239 extended), with the same min-height/min-width: 48px plus inline-flex centring the other 48 px controls use — their labels are smaller than the box they now sit in. Touch only; desktop is untouched. No colours added, sizes and display only.

.col-rehide-pill carries .col-hidden-pill as well, so the "hide" affordance after a reveal gets the same 48 px.

test-touch-targets.js gets both, rendered in their real parent, because that is what decides the box: "Got it" is a flex item of .gesture-hint-inner, and the reveal pill is appended into a <th> of the packets table by TableResponsive.apply().

Measurements

iPhone-class touch context, 375×812, against a local Go server on the e2e-fixture.db prepared the way CI prepares it:

Element master this branch desktop (fine pointer, both)
.gesture-hint-dismiss 34.7 × 26 59.3 × 48 59.3 × 26
.col-hidden-pill 23.0 × 18 75.0 × 48 75.0 × 18

The mobile widths differ from the harness's 48 × 48 because the real labels ("Got it", "+8 hidden") are wider than the minimum.

Navigating #/channels → #/live → #/channels five times, scrolling the message pane on each visit and then dispatching a scroll at the pane that had just left the document: 0 console errors, mobile and desktop. On master the last step is the reported TypeError.

Not in the diff

cmd/server and the rest of the Go tree are untouched; no new map[string]interface{}. .github/ is not in the diff — fork guards stay at 9 in deploy.yml and 1 in release-fast-path.yml. No npm dependencies added. scripts/check-xss-sinks.sh --diff origin/master is clean, and scripts/check-css-vars.js reports 0 undefined vars.

…ts (#314)

Relates to #314. Two pre-existing UI defects found in a browser check of
v0.2.2-rc1; neither is a regression.

1. public/channels.js registered an anonymous `scroll` listener on
   #chMessages whose body did an unguarded
   document.getElementById('chScrollBtn').classList.toggle(...). The router
   destroys the page and lets the next init() rebuild #app, so a `scroll`
   can still reach the detached pane after #chScrollBtn has left the
   document, and the listener threw. The lookup is now optional-chained like
   every other #chScrollBtn call site in the file, and the handler is a
   stable module-level function so init() can drop the pane it held and
   destroy() can take the listener off again -- at most one live listener,
   the same leak class as #259/#282 scoped to the element.

2. .gesture-hint-dismiss ("Got it", 26 px) and .col-hidden-pill (18 px)
   join the Kpa-clawbot#630/#239 coarse-pointer block at the 48 px house minimum, with
   the flex centring the other 48 px controls use. Touch only; desktop keeps
   26 px and 18 px.

Tests: test-issue-314-chscroll-listener.js (new, registered in test-all.sh)
drives the real channels.js in a vm sandbox that models the router's #app
rebuild; test-touch-targets.js covers both controls in their real parents.
@dborup

dborup commented Oct 7, 2026

Copy link
Copy Markdown
Owner Author

Rapport — CS-Minimax PR#331 #314 — head 256e37e

Status: Both points done, each with a test that is red on origin/master and green here plus killed mutants; CI green on every job, no reruns needed.

Evidence tags: [T] test run, [A] automated gate/CI, [K] code reading or browser measurement.

Requirement 1 — the chScrollBtn scroll listener must not throw after leaving the page

Item Evidence
Fix public/channels.js: the handler is a stable module-level onChMessagesScroll, reads the pane off event.currentTarget, and optional-chains the #chScrollBtn lookup the way the file's four other call sites already do. The #chScrollBtn click registration on the next line is optional-chained too. [K]
Leak check (#259/#282 class) Yes, but element-scoped rather than document-scoped: every visit left one detached #chMessages holding a live handler. init() now drops the pane it held before taking the new one and destroy() removes the listener — at most one live listener whatever the call order. [K]
Test test-issue-314-chscroll-listener.js (new, 8 cases), registered in test-all.sh. Drives the real channels.js in a vm sandbox that models the router's #app rebuild, so a second init() really gets a new #chMessages. Red on master: 4 of 8 fail, including Cannot read properties of null (reading 'classList') on the detached dispatch and 4 live listeners after four visits. Green here: 8/8. [T]
Acceptance case from the issue a scroll event on a detached #chMessages does not throw — asserts the pane is really detached, then dispatches. [T]
Mutant A Drop the ?. in the handler → a scroll event on a detached #chMessages does not throw fails (7 passed, 1 failed). [T]
Mutant B destroy() stops removing the listener → destroy() removes the scroll listener fails (7/1). [T]
Mutant C init() stops dropping the previous pane → a re-init without a destroy leaves no handler on the old pane fails (7/1). [T]

Requirement 2 — .gesture-hint-dismiss and .col-hidden-pill at 48 px under pointer: coarse

Item Evidence
Fix Both selectors join the existing @media (pointer: coarse) block in style.css (the Kpa-clawbot#630 group #239 extended): min-height/min-width: 48px plus the inline-flex centring the other 48 px controls use. Sizes and display only — no colours, no new CSS variables. .col-rehide-pill carries .col-hidden-pill, so the post-reveal "hide" affordance is covered as well. [K]
Test test-touch-targets.js extended with both, each in its real parent (the "Got it" button as a flex item of .gesture-hint-inner; the pill inside a <th> of .data-table, where TableResponsive.apply() puts it). Red on master: 34.7x26.0 and 23.0x18.0, matching the issue's measurements. Green here: 48.0x48.0 for both. [T]
Mutant D Remove .gesture-hint-dismiss from the rule → that row fails at 34.7x26.0, 1 assertion. [T]
Mutant E Remove .col-hidden-pill from the rule → that row fails at 23.0x18.0, 1 assertion. [T]
Mutant F 48 px → 44 px → both rows fail at 44.0x44.0, 2 assertions. [T]

Browser check

Chromium against a local Go server on the e2e-fixture.db, prepared the way the pipeline prepares it.

Element mobile 375×812, touch desktop, fine pointer
.gesture-hint-dismiss ("Got it") 59.3 × 48.0 (was 34.7 × 26.0) 59.3 × 26.0, unchanged
.col-hidden-pill ("+8 hidden") 75.0 × 48.0 (was 23.0 × 18.0) 75.0 × 18.0, unchanged

Rendered widths exceed 48 px because the real labels are wider than the minimum. [K]

#/channels → #/live → #/channels five times, scrolling the message pane on each visit and then dispatching a scroll at the pane that had just left the document: 0 console errors, on both the mobile and the desktop context. On master the final step produces the reported TypeError. [K]

Local suites

Suite Result
Go, all 18 modules (go test ./... per module — this is a multi-module repo, there is no root go.mod) all ok, 0 failures [T]
sh test-all.sh 226 files, 226 passed, 0 failed (includes test-test-all.js, which confirms the new file is registered exactly once) [T]
node test-frontend-helpers.js 709 passed, 0 failed [T]
E2E — gestures/touch test-gestures-1062-e2e.js, test-gestures-1185-scroll-discriminator-e2e.js, test-gesture-hints-1065-e2e.js, test-touch-gestures-coverage-e2e.js, test-issue-2052-touch-target-e2e.js — all pass [T]
E2E — channels test-issue-1367-channels-chat-app-e2e.js, test-issue-1224-channels-mobile-ux-e2e.js, test-channels-list-render-e2e.js, test-channels-selection-flow-e2e.js, test-channels-154-155-e2e.js, test-channel-fluid-e2e.js, test-channel-modal-e2e.js, test-channels-ws-batch-e2e.js, test-channels-client-state-152-e2e.js — all pass [T]
E2E — packets layout / column hiding test-issue-258-column-widths-e2e.js, test-issue-1128-packets-layout-e2e.js, test-issue-96-hide-control-e2e.js, test-issue-1122-packets-filter-ux-e2e.js, test-table-fluid-e2e.js, test-issue-1128-multi-viewport-e2e.js — all pass [T]
E2E — a11y test-a11y-axe-1668.js, test-issue-1668-m2-contrast.js, test-issue-1668-m3-typography.js — all pass [T]

CI per job (run 37579490801, head 256e37e7)

Job Result
Go Build & Test pass, 17m51s [A]
Playwright E2E Tests pass, 25m32s [A]
Build & Publish Docker Image pass, 48s [A]
Release Artifacts skipped (not a release) [A]
Deploy Staging skipped [A]
Publish Badges & Summary skipped [A]

No reruns. The known flaky test from #271 did not fire.

Gates

Gate Result
scripts/check-xss-sinks.sh --diff origin/master clean, exit 0 [A]
node scripts/check-css-vars.js OK — 2177 var() refs, 172 definitions, 0 undefined [A]
Fork guards 9 in deploy.yml, 1 in release-fast-path.yml, unchanged; .github/ is not in the diff [K]
cmd/server not in the diff; no Go file changed, so no new map[string]interface{} [K]
npm dependencies none added [K]
Hardcoded colours none; the new CSS sets sizes and display only [K]

Remaining

  • The packets table header is taller on mobile, because the reveal pill now occupies 48 px inside the <th> instead of 18 px. That is the intended consequence of the 48 px minimum, and it is the same trade-off fix(css): raise the remaining 44 px controls to the 48 px touch-target minimum (#235) #239 accepted for the channel sidebar header. An invisible ::after tap pad would have kept the visual size, but the pad would overlay the neighbouring sort target in the same header cell, so the visible pill is the safer option. Say the word if you prefer the pad.
  • "Got it" is now 48 px wide at minimum, so in the narrow swipe tip the hint text wraps to one more line at 375 px. The tip is transient and stays inside the viewport.
  • The @media (pointer: coarse) rule covers .col-rehide-pill through .col-hidden-pill. There is no separate test row for it; it shares the selector under test.

…croll-touch-targets

# Conflicts:
#	test-all.sh
@dborup

dborup commented Oct 7, 2026

Copy link
Copy Markdown
Owner Author

Rapport — CS-Minimax PR#331 master-sync — head bac0819

Merged origin/master (6444294) into this branch with a merge commit. No rebase, no amend, no force-push; the push was a fast-forward. The PR stays a draft.

Conflicts

One real conflict, in test-all.sh. Both sides appended a run line at the same spot, right after run test-issue-282-comment-guards.js:

Resolved by keeping both, in issue-number order (314, then 322) to match how the surrounding block is ordered. Master's other addition from #318, run test-estimated-positions-config.js further down the file, merged cleanly and is untouched. test-test-all.js enforces that every registered file exists, that none is registered twice and that no E2E sneaks in — it passes, so the resolved list is well-formed.

The other two shared files auto-merged, with both sides landing in different regions:

test-touch-targets.js was not touched by master, so it merged with no conflict.

Tests

Run against the merge commit:

  • node test-frontend-helpers.js — 709 passed, 0 failed
  • sh test-all.sh — 228 passed, 0 failed (228 files), including test-issue-314-chscroll-listener.js, test-touch-targets.js and master's two new registrations
  • go build for server, migrate and ingestor — all three build, so master's Go changes compile on the merge

E2E against a local Go server on :13581 with e2e-fixture.db, prepared exactly as the e2e-test job does (freshen-fixture, the Kpa-clawbot#1486 and Kpa-clawbot#1791 seed rows, corescope-migrate, the 2073/199/245 seed SQL, public-instrumented):

  • test-e2e-playwright.js — 132/135 passed, 3 skipped
  • test-a11y-axe-1668.js — net=0 across 120 cells (2 viewports x 2 themes x 30 routes)
  • touch-target / table: test-issue-2052-touch-target-e2e.js 19 passed, test-gesture-hints-1065-e2e.js 15 passed, test-touch-gestures-coverage-e2e.js 12 passed, test-table-fluid-e2e.js 41 passed
  • channels (both sides of the channels.js merge): test-channels-observed-path-hash-size-e2e.js 8, test-channels-client-state-152-e2e.js 6, test-channels-client-state-152-decrypt-e2e.js 18, test-channels-ws-batch-e2e.js 6, test-channels-selection-flow-e2e.js 8, test-channel-issue-1087-e2e.js 3, test-channel-issue-1111-e2e.js 2, test-channels-154-155-e2e.js 8, test-channel-fluid-e2e.js 15 — all 0 failed

test-channels-selection-flow-e2e.js covers "scroll button exists and toggles hidden when scrolled to bottom", which is the behaviour the rewritten #chMessages listener is responsible for.

Browser check — 375x812, touch

Chromium at 375x812 with hasTouch/isMobile, measuring the real boxes and tapping with the touchscreen (not a synthetic click):

  • swipe tip "Got it" — 59.3 x 48.0 px, display:flex with align-items:center / justify-content:center, fully inside the viewport, the box centre hit-tests to the button itself, and a touch tap removes the hint and persists it as seen. Checked for both bottom hints (row-swipe and tab-swipe).
  • hidden-column pill on the packets table — "+8 hidden", 75.0 x 48.0 px, display:inline-flex and centred, centre hit-tests to the pill, and a touch tap takes the hidden <th> count from 8 to 0 (the OBSERVER column appears and the pill goes away).

No page errors or console errors during the run.

One note for whoever picks up the hint UI next, unrelated to this PR: scheduleHints() shows every relevant unseen hint at once, and every .gesture-hint-bottom is pinned at the same bottom:80px; left:50%, so the two bottom hints render stacked on top of each other. That predates this branch and is untouched here; the check above isolates one hint at a time so the tap is unambiguous.

CI

Run 37590366645 on the merge commit — completed / success. Per job:

Job Result Duration
✅ Go Build & Test success 25m47s
🎭 Playwright E2E Tests success 24m58s
🏗️ Build & Publish Docker Image success 56s
📦 Release Artifacts skipped tag-gated (refs/tags/v*)
🚀 Deploy Staging skipped gated to push on master in the upstream repo, so a PR run never reaches the staging runner
📝 Publish Badges & Summary skipped push only

The three jobs that run for a pull request all pass; the three skips are the workflow's normal PR behaviour, not failures. The PR is no longer conflicting and remains a draft.

@dborup-agent

Copy link
Copy Markdown
Collaborator

Review — CS-pve-agent2 PR#331 — head bac0819

Dom: APPROVE med nits

Independent, read-only review. Both points of #314 are fixed, each with a test that is red on origin/master and green on the head; the master sync lost nothing; the browser behaves as the PR says on mobile and desktop. The nits below are test-coverage gaps and one wording slip, none blocking.

Evidence tags: [T] a test I ran, [A] automated gate or CI, [K] code reading or my own browser measurement.

Findings

# Severity Where Finding Evidence
1 Nit test-touch-targets.js The harness asserts only the 48×48 box. The inline-flex centring and the "touch-only, desktop untouched" claim are not asserted: removing display/align-items/justify-content (C1) and copying the rule outside the @media (pointer: coarse) block (C3) both survive. I checked both in the browser instead (see below), so this is a gap in the guard, not a defect. [T] [K]
2 Nit public/channels.js:1686 The new ?. on the #chScrollBtn click registration is not exercised by any test (M1 survives: the harness always fabricates the button during init()). It is defensive and behaviour-neutral, so fine to keep untested; just noting it. [T]
3 Nit public/channels.js:278 autoScroll = atBottom was carried over into the new handler but nothing asserts it (M4 survives in both the new unit test and test-channels-client-state-152.js). This is pre-existing behaviour and is preserved correctly. A one-line assertion in the "toggles while the page is live" case would pin it. [T] [K]
4 Nit PR body It says "all four other #chScrollBtn call sites"; there are five (channels.js lines 402, 1914, 2718, 2747, 2853 on the head, all optional-chained on master too). The code comment says "every other", which is correct. Cosmetic only. [K]
5 Info Packets table, mobile The header <th> holding the reveal pill grows from 25 px to 55 px at 375×812. This is the expected consequence of a 48 px pill and the author already flagged it under "Remaining". [K]

1. Scroll listener

  • The fix. The handler is a stable module-level onChMessagesScroll. It reads the pane off e.currentTarget and optional-chains the button lookup. init() removes the listener from any previously held pane, and destroy() removes it and nulls _chScrollEl. [K]

  • Red before, green after. I ran test-issue-314-chscroll-listener.js against master's channels.js: 4 passed and 4 failed. [T] The failing cases were:

    • "detached scroll does not throw";
    • "destroy() removes";
    • "several visits leaves one live listener" (got 4);
    • "re-init without destroy" (got 1).

    On the head all 8 pass. [T] The issue's acceptance test is the detached-dispatch case. It deliberately skips destroy(), so it exercises the ?. guard and not just the removal.

  • At most 1 listener. I counted in a real browser with CDP DOMDebugger.getEventListeners, after 5 rounds of #/channels → #/live and a final #/channels. [K]

    • On the head the live pane has 1 scroll listener and each of the 5 detached panes has 0.
    • On master the live pane has 1 and each detached pane still has 1. That is the element-scoped leak the PR describes.

2. Tap targets

  • The rule. Both selectors were added to the existing @media (pointer: coarse) block in style.css, with min 48×48 and inline-flex centring. [K]
    • The base .gesture-hint-dismiss rule sits later in the file at the same specificity, but it sets no display or min-*, so there is no cascade conflict.
    • The base .col-hidden-pill display: inline-block comes earlier, so the media rule wins.
    • TableResponsive.unhidden() hides pills with inline style.display = 'none', which still beats the stylesheet inline-flex.
  • test-touch-targets.js. On master, with the PR's test file: 34.7x26.0 and 23.0x18.0, FAIL with 2 assertions. On the head: 48.0x48.0 for both, OK. [T] In CI the Playwright job logs both rows at 48.0x48.0 with CHROMIUM_REQUIRE=1. [A]

3. Master sync

4. Browser check

Chromium against a local Go server on the e2e-fixture.db, prepared as in CI (freshen, the Kpa-clawbot#1486/Kpa-clawbot#1791 seed SQL, corescope-migrate, then seeds 2073, 199 and 245). For comparison, a second server instance served master's public/ against the same DB. [K]

head, 375×812 touch (pointer: coarse = true) master, 375×812 touch head = master, 1440×900 fine pointer
"Got it" (.gesture-hint-dismiss) 59.3 × 48, flex, centred 59.3 × 26 59.3 × 26, block
"+8 hidden" (.col-hidden-pill) 75.0 × 48, inline-flex, centred 75.0 × 18 identical on both, inline-block
Errors over 5 × #/channels → #/live → #/channels, scrolling the pane and then dispatching scroll on the pane that just left the document 0 5 × Cannot read properties of null (reading 'classList') head 0, master 5 (desktop)

The desktop column was measured by injecting both controls into #/packets at 1440×900, because the app shows neither one there. Head and master gave identical boxes, so the change really is touch-only.

5. Gates

Gate Result
scripts/check-xss-sinks.sh --diff origin/master clean, exit 0 [A]
node scripts/check-css-vars.js OK, 0 undefined [A]
Hardcoded colours in the diff none; the CSS sets sizes and display only [K]
cmd/, internal/, .github/ in the diff none, so cmd/server stays read-only and there are 0 new map[string]interface{} [K]
Fork guards (== 'Kpa-clawbot/CoreScope') 9 in deploy.yml, 1 in release-fast-path.yml [K]
Closing keywords in the PR body or commits none; the body uses "Relates to #314" [K]
Commit author and committer dborup <kontakt@meshview.dk> on both commits [K]
Behaviour beyond the issue none found: the listener rewrite keeps the < 60 threshold and the autoScroll update, and the CSS is touch-scoped [K]

Tests and mutants

Everything below ran on the head tree, which is also the merged tree against origin/master 6444294d. The re-run against the newer master follows the table.

Suite Result
sh test-all.sh 228 files, 228 passed, 0 failed [T]
node test-frontend-helpers.js 709 passed, 0 failed [T]
cmd/server and cmd/ingestor, go test ./... ok github.com/corescope/server (895 s) and ok github.com/corescope/ingestor (1434 s), with -timeout 30m [T]. My first run used Go's default 10 m timeout while the E2E suites were running alongside, and both packages hit that limit. CI uses -timeout 20m, so I reran on their own with a longer limit.
E2E, channels all 0 failed [T]: test-channels-selection-flow-e2e.js 8, test-channels-client-state-152-e2e.js 6, test-channels-154-155-e2e.js 8, test-channel-fluid-e2e.js 15, test-channel-issue-1087-e2e.js 3, test-channel-issue-1111-e2e.js 2, test-channels-ws-batch-e2e.js 6, test-channels-observed-path-hash-size-e2e.js 8
E2E, touch and table all 0 failed [T]: test-gesture-hints-1065-e2e.js 15, test-touch-gestures-coverage-e2e.js 12, test-issue-2052-touch-target-e2e.js 19, test-table-fluid-e2e.js 41, test-issue-96-hide-control-e2e.js 10, test-issue-258-column-widths-e2e.js 22
test-e2e-playwright.js, fail-fast disabled in a scratch copy head 131/135, 3 skipped, 1 failed. Master's frontend on the same server and DB gave 131/135, 3 skipped, and the same 1 failure. [T] The failing test is "Version info lives on Perf dashboard". #navStats did not fill within 10 s in my environment; it reproduces identically on master and is not touched by this PR.

Re-run against the newer origin/master 5cdeffd5. Master moved while I was reviewing; it now includes #343, #346 and #38. The head is unchanged.

  • git merge-tree --write-tree origin/master bac08199 merges cleanly, to tree eab69c76. Its diff against 5cdeffd5 is exactly the PR's 5 files and +406/−6. [K]
  • Of the PR's files, master's new commits touch only test-all.sh, which auto-merges, and public/live.js (Evaluate Live node-filter readiness guard (upstream 2103) #220), which is on the navigation path I tested.

On eab69c76:

Suite Result
node test-frontend-helpers.js 709 passed [T]
sh test-all.sh 228 of 229 files passed [T]
go test -timeout 30m/40m ./... ok github.com/corescope/server (795 s) and ok github.com/corescope/ingestor (896 s) [T]
E2E (local server built from eab69c76, same fixture prep) all 0 failed [T]: test-issue-220-live-filter-readiness-e2e.js 11, test-channels-selection-flow-e2e.js 8, test-channels-client-state-152-e2e.js 6, test-channel-fluid-e2e.js 15, test-gesture-hints-1065-e2e.js 15, test-issue-2052-touch-target-e2e.js 19, test-table-fluid-e2e.js 41, test-issue-96-hide-control-e2e.js 10
test-touch-targets.js OK, both rows at 48.0x48.0 [T]
Browser check same as above: mobile boxes 59.3×48 and 75×48, 0 errors over 5 round trips, and 0 listeners on the 5 detached panes, on both mobile and desktop [K]

The one test-all.sh failure was test-channels-client-state-152.js, case "N1: a remap mid-decrypt restarts loading…". It ran while a Go suite was loading the machine. It is a timing flake that already exists on master, not something this PR introduces: [T]

  • 3 isolated reruns pass 75/75.
  • Under the same load it failed 1 of 25 runs on the merged tree and 1 of 25 on master 5cdeffd5.
  • In a further 30 runs on master it was the same N1 case that failed.

Mutants of my own, beyond the author's A–F:

Mutant Result
M2: register (e) => onChMessagesScroll(e), a fresh closure, instead of the stable reference killed: 3 cases fail (destroy, repeat visits = 4, re-init) [T]
M5: destroy() no longer removes the listener, and init() no longer drops the previous pane killed: 3 cases fail [T]
C2: drop min-width, keep min-height killed: both rows fail at 34.7x48 and 23.0x48 [T]
C4: rule targets .col-rehide-pill instead of .col-hidden-pill killed: the pill row fails at 23.0x18 [T]
M1: drop the ?. on the click registration survives (finding 2) [T]
M3: handler reads _chScrollEl instead of e.currentTarget survives. It is effectively equivalent here: while the listener is attached, _chScrollEl is the pane it is attached to. [T]
M4: drop autoScroll = atBottom survives (finding 3) [T]
C1: drop the inline-flex centring survives (finding 1) [T]
C3: copy the rule outside the coarse block, so it leaks to desktop survives (finding 1) [T]

CI run 37590366645 on bac08199 is a single attempt with no reruns. [A]

Not verified

  • The full CI Go matrix, meaning modules other than cmd/server and cmd/ingestor. The PR touches no Go code.
  • In my browser script, the #chScrollBtn show/hide while scrolling did not reproduce on either head or master, because my injected filler did not make the pane scrollable. That behaviour is instead covered by the head's unit test and by test-channels-selection-flow-e2e.js ("scroll button exists and toggles hidden"), which passed.
  • The navStats E2E failure is environmental and identical on master; I did not chase its cause.
  • Real iOS or Android hardware: I used only Chromium device emulation with hasTouch/isMobile.
  • Staging and production were not touched.

@dborup
dborup marked this pull request as ready for review October 7, 2026 12:02
@dborup
dborup merged commit f490ac6 into master Oct 7, 2026
6 checks passed
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.

2 participants