Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/81-keyboard-nav-harness"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The next-sprint bundle: two keyboard-navigation fixes + the regression harness that reveals them. Built TDD-shape — the bugs are the harness's first failing rows, so the harness is the regression-vehicle the gaps revealed it was missing, not an afterthought suite.
#79 — multiplayer name-entry keyboard-trap
Entering a name on the versus checkin screen had no keyboard escape (only a page reload). Fix: on
checkin,Esc → title(welcome). No connection has opened yet (startMatchdoes that on Enter), so it's a pure screen-state reset; the render loop hides+blurs the #66 DOM name-input on the screen change.Enter → proceedunchanged. (AC: no trap, fresh welcome, Enter-path intact.)#80 — initials live after commit (duplicate-submit tail)
After committing initials, the input stayed editable — Backspace kept editing, Re-Enter re-submitted. Engineer's probe confirmed
leaderboard.go submit()appends (no dedup), so Re-Enter planted a duplicate row — a real correctness tail, not just UX. (And a content-dedup guard would be wrong: legit ties on identical initials+score are valid — so the fix is client-source-kill, not server-dedup.)Root cause: the phase only advanced to
'submitted'after the async submit resolved, leaving an'entering'window where the gates stayed live. Fix: a new synchronous'submitting'phase, set the instant Enter commits — before the network call — so the keydown + click gates (which key off'entering') go inert immediately, killing the duplicate-submit at source. Success →'submitted'; failure → back to'entering'(retry). Render maps'submitting' → 'entering'so its phase union is untouched.Lands on Herald's ratified A: the existing
'submitted'state is the leaderboard-display reveal (highlighted entry + SCORE SAVED + PLAY AGAIN→new solo / BACK→title) — no separate screen needed (his B collapsed into A once the actual rendered state was source-grounded).#81 — keyboard navigation-tree harness (
@playwright/test)A declarative transition table — each row is
(node, key) → expected nav-state— driving the real state machine, plus a completeness meta-check that fails loud if any reachablenode × keycell lacks a row (this caught theinitials-submitting × Escapegap during authoring — the teeth working by construction, not author-discipline).Two
?navtest-gated seams inmain.ts(same URL-flag convention as?mock/?debug=audio):window.__navState— read-only assert (canvas screens expose no DOM to read);window.__navTest— arranges the solo-gameover initials node, which is server+gameplay-gated and can't be reached by navigation alone.Scope: critical reachable path × {Enter, Escape, Backspace}. Multiplayer lobby/match nodes need a WS mock → v2 (asserted-as-skipped, logged not silent).
CI (#83)
New
client-navjob runs the harness on QM's dedicatedplaywrightrunner (mcr.microsoft.com/playwright:v1.61.0-jammy, browsers pre-baked at/ms-playwright).@playwright/testpinned exactly1.61.0to match the image. The existinggoclient tsc job is unaffected — 1.61 has no postinstall browser download, so the new devDep doesn't slow/breaknpm ci. This PR's own CI run exercises the new job = the suite running green in real CI.Decision tree
'submitting'phase vs a boolean flag: the phase is the honest model — it also lets the restart-Enter handler distinguish "mid-submit" (inert) from'submitted'(PLAY AGAIN). A flag leftcommitted && enteringas an inconsistent combo.__navTestarrangement seam vs play-to-gameover: the initials node needs a real server + topping out — impractical + nondeterministic in a harness. A?navtest-gated arrangement helper is deterministic; gated so it's inert in normal play (same risk class as?mock).What this PR does NOT do
gorunner — the harness needs browsers; it runs on theplaywrightrunner only.Verification
Harness 13/13 green in both forms (raw playwright during dev + the committed
@playwright/test). Mutation-proven: reverting #79 redscheckin × Escape(screen stuck on checkin); reverting #80 reds theinitials-submittingrows (Backspace edited post-commitAB, no inert state). tsc + build clean. Ceiling: the iOS-keyboard-summon (#66) and real-device touch remain on-device operator gates; this harness is the desktop-keyboard axis.Closes #79
Closes #80
Closes #81
Part of #83
🤖 Generated with Claude Code
Surveyor review — APPROVED ✅ (independently verified, head
367749ab)Strong PR. Correct fixes, a harness with genuine teeth, real-CI-green. I reproduced the load-bearing claims rather than trusting them — verification log below. One non-blocking should-consider (a keyboard/touch guard asymmetry) for your judgment.
What I verified (not just read)
screen='title'→'checkin'on checkin-Escape) →checkin × Escapereds, 12 others pass.initialsPhase='submitting'atmain.ts:408) → 4 rows red (theentering→commit→submittingtransition + all threeinitials-submittingrows). Slightly more teeth than the PR body claims.git checkout); 13/13 restored,git diffempty.367749ab:client-nav: success(15s) +server+client (tsc)— the new job genuinely exercised the harness on QM'splaywrightrunner.merge_base == base.sha == main tip; only open PR — no composition concern.#79 — checkin Esc→title
Correct. No connection is open yet (
startMatchopens it on Enter), so it's a pure screen-state reset; the render loop hides+blurs the #66 DOM input on the screen flip → no trap.namepersists. Enter-path unchanged.#80 — synchronous
'submitting'phase — gate-completeness auditedWalked every
initialsPhasereader. All correctly treat'submitting'as inert:593, keyed off'entering') → A-Z/Backspace/Enter-commit/Escape-abandon all inert during submitting;621) explicitly guards!== 'submitting';785, keyed off'entering') → inert;submitting→entering(942) so the phase union is untouched;submitInitialssets'submitting'synchronously at408before thevoid submitLeaderboardawait — closing the re-fire window at source; reverts to'entering'on!res.ok(378) and catch (388); blocked-initials returns early without going inert (correct — retry stays live).The duplicate-submit is killed on both submit paths (keyboard Enter-commit + touch SAVE). The named bug is solidly fixed.
Should-consider (non-blocking): touch PLAY AGAIN restart lacks the guard the keyboard restart has
The keydown restart-Enter (
621) was deliberately guarded&& initialsPhase !== 'submitting'. The touch rematch button is not:inRematchButton(795) has noinitialsPhasecheck,REMATCH_BTNis drawn unconditionally on the gameover leaderboard (render.ts:1323/1466), and the hit-test is pure geometry. So during'submitting'(rendered as'entering'), a tap on PLAY AGAIN restarts while the submit is in-flight — whereas keyboard-Enter-restart is inert.785already handles that) — thevoidsubmit still completes, score still saved, no dup.leaderboardError(377/387) afterresetLeaderboardcleared it (338) — andfetchLeaderboard's success path (355–360) doesn't clear it — so a stale "score not saved" banner can leak onto the next gameover. (Traced from source, not run — the harness is keyboard-only so this path is uncovered.)&& initialsPhase !== 'submitting'to795for keyboard/touch symmetry (one line; if you fold it I just re-confirm the head SHA, no full re-review), or (b) accept-as-conscious-line if the touch-restart-abandons-during-submit is intended — defensible, since PLAY AGAIN is a distinct deliberate button (no commit+restart key-overload like keyboard Enter). For a mobile-primary game the touch path is the primary one, so worth a deliberate call rather than an implicit one.Bundle-vs-split — endorse the bundle
It's a genuine TDD unit: the harness's value is that the two fixes are its first failing rows. Splitting forces either a red-CI harness-first PR or a merge-unverified fixes-first PR. One coherent story; the operator framed it as one zero-open sprint. Keep it bundled.
?navtestseam — soundVerified truly inert without the flag (the whole
__navState/__navTestblock is insideif (NAV_TEST);NAV_TEST = location.search.includes('navtest')).__navTest.enterInitials()faithfully reproduces the real node (solo + gameover +'entering'+ 3-space pending + populated board at rank 3) — so the harness drives the genuine state machine, not a divergent fake. The arrangement-seam scope (beyond a read-only getter) is justified — the initials node is server+gameplay-gated, impractical/nondeterministic to reach by play — and it's the same risk class as the existing?mock. No concern.Nit
fetchLeaderboardsuccess path doesn't clearleaderboardError— it's the stale-banner vector in the should-consider above; clearing it on success would harden that path regardless of the795decision.Disposition: APPROVED. Core deliverable is correct + verified end-to-end. The should-consider is yours to fold-or-accept; neither blocks merge. Routing back to you for the call, then merge-ready→Bosun. 🔧
Surveyor re-stamp — APPROVED ✅ (re-pinned to
d13b63f)Bounded-delta re-confirm of my prior APPROVED (367749a) after the should-consider (a) fold. A code commit moved the head, so the prior stamp is superseded by this one; the prior review's substance carries forward unchanged for everything outside the delta.
Delta verified
367749a..d13b63f(2 files, +28/−1 — nothing unexpected):main.ts:795— touch rematch guard gains&& initialsPhase !== 'submitting', mirroring the keyboard restart-Enter guard (621). Exactly the fold.nav.spec.ts— new test#80: touch PLAY AGAIN inert during submitting, clicking the REMATCH_BTN centre (345,499 in the 960×640 canvas — geometrically correct for the Desktop-Chrome landscape render path) and asserting no restart. Cold-start determinism waits present.Reproduced (same standard as the original):
d13b63f→ 14/14 green locally, incl. the new touch test.git diffempty.d13b63f:server+client-nav+client (tsc)— the new touch test ran green in real CI.The fold closes the keyboard/touch asymmetry exactly: the touch submit now resolves before any restart can fire, so the skipped-reveal + stale-error-banner leak vector is gone — and #80 now has both-paths (keyboard + touch) regression coverage. Good fold.
Disposition: APPROVED, merge-ready. No open items on my axis. → Bosun for the merge gate. 🔧