mirror of
https://github.com/tiennm99/noitu.git
synced 2026-10-19 08:15:25 +00:00
133 lines
14 KiB
Markdown
133 lines
14 KiB
Markdown
# Web fixes from the review
|
|
|
|
Branch `dev`, 2026-09-29. Not committed. Only files under `web/` changed. Playwright was not run (no browser), and no e2e spec was added or changed.
|
|
|
|
## Gates
|
|
|
|
`cd web && npm run check && npm run lint && npm test`: check 0 errors and 0 warnings (386 files), lint clean, 326 tests passed in 18 files (was 270 in 16). `tests/bundle.test.js` is in that run and passes.
|
|
|
|
Fail-without-fix check: I reverted each behavioural fix one at a time and ran its test file. Every revert made at least one test fail, and each file was restored afterwards. The reverts were:
|
|
- `session_not_resumable` in `LEAVES_ROOM`
|
|
- the `iAmOut` row check
|
|
- the ordinal reuse in the chat history
|
|
- the numbering in `chainToText`
|
|
- `freshSession`
|
|
- the `game.leave()` on a refused resume
|
|
- the `onResumeRefused()` call in `client.js` (4 tests fail)
|
|
- the shortest-RTT clock filter
|
|
- the GameOverPanel focus guard
|
|
- the ChatPanel refused-send guard
|
|
- the ChatPanel offline disable
|
|
- the away banner's `role="status"`
|
|
|
|
Finding 1 is the exception: it needed no client change, so there was nothing to revert.
|
|
|
|
## Per finding
|
|
|
|
1. **Game ended while away.** No client change was needed. The reducer already handles the replay sequence from `playing`: `RoomState` fills in the room and leaves the phase alone, `ChatHistory` replaces the chat, and `GameOver` then moves the phase to `over` with the result and standings. The lobby actions appear because `over` renders the Lobby. Three store tests pin it: the exact sequence lands on `over` with standings, the room snapshot survives (so Ready and Leave are available), and a mid-game `RoomState` alone does not end the game. These tests pass on the unmodified reducer, so they are regression pins and not fail-without proofs.
|
|
2. **Refused resume.**
|
|
- `session_not_resumable` is now in `LEAVES_ROOM` (`game-apply.js`).
|
|
- `client.js` tracks whether the current Hello carried a token. After the Welcome, an `error` that arrives before any other non-Pong frame is a refused resume. The client then drops the spent token and calls the new `onResumeRefused` option. Pongs are skipped because the client pings right after Hello. `connection.svelte.js` wires the option to `game.leave()`. It runs before the error is forwarded, so the board clears and the error banner still shows.
|
|
- Tests in `ws-client.test.js` cover: both codes, a Pong in between, an error after the restored room (no report), a Hello without a token (no report), a spent token, and error forwarding. Two tests in `connection.test.js` and one in `game-store.test.js` cover the rest.
|
|
- On `/play`, an in-game refusal now lands on an idle board with the error banner. I did not add a "start again" button.
|
|
3. **`iAmOut`.** It is now true when `elimination !== null` or the player's own row in `gamePlayers` is eliminated. Two tests: out from the row alone, and another player's elimination does not count.
|
|
4. **`/play` reload.**
|
|
- `connect({ freshSession: true })` forgets the stored token before a new client is created. `startGame()` uses it.
|
|
- I did not call `forgetSession()` unconditionally before `connect()`. A rematch calls `startGame()` again on the socket that already exists. Forgetting there would delete the token this game's Welcome had just stored, and a later socket drop would open a fresh session instead of resuming the game. The flag only acts when a new client is created, so an existing socket keeps its token.
|
|
- `forgetStoredSession()` is now a module-level export in `client.js`, and the client's `forgetSession` is the same function. It is needed because there is no client yet on the first mount.
|
|
- `connection.test.js` checks the first Hello carries no token with the flag, carries the stored token without it, and that a second `connect({freshSession})` on a live socket keeps the Welcome's token.
|
|
- The `+page.svelte` call site itself is not covered, because there is no page-level test harness.
|
|
5. **Held action vs resume.** In `online/+page.svelte` the flush effect now waits for the resume's `RoomState` before flushing a held action, if the socket dropped while in a room.
|
|
- It notes the roster when the socket drops. After reopening, it releases the action once the roster object changes, since each `RoomState` replaces it.
|
|
- If the resume is refused and the room is gone, the held action is cleared instead of sent.
|
|
- An action held outside a room (cancelling the queue) is still flushed as soon as the socket opens.
|
|
- This is page logic and has no unit test. It needs a human or e2e check.
|
|
6. **Leave while offline.** `leave()` now sends `LeaveRoom` if the socket can carry it and otherwise drops it, never holds it. It also clears any other held action. The seat is freed by grace expiry either way.
|
|
7. **Chat offline.** `onsend` returns a boolean and `say()` returns `send()`'s result. The draft is cleared only when the send went out. The send button is disabled while `connection.status !== OPEN`, and `submit()` also refuses offline. New tests: a refused send keeps the text, and the button is disabled and nothing is sent while reconnecting. The existing chat tests now set the connection to open.
|
|
8. **Away countdown.** Each banner's `role="status"` span holds only "X mất kết nối…". The seconds sit in a sibling `aria-hidden` span. `data-testid="away-…"` is on the outer `<p>`, so the e2e `toContainText('… mất kết nối')` still matches. `vi.js` swaps `playerDisconnectedIn` for `playerDisconnectedSeconds: '({n}s)'`.
|
|
9. **GameOverPanel focus.** New `src/lib/focus.js` exports `typingElsewhere(own)`. WordInput imports it instead of its private copy, and GameOverPanel only focuses when the player is not typing elsewhere. Tested with a focused external input.
|
|
10. **Clock offset.** The client keeps the last 5 pong samples `{rtt, offset}` and uses the offset of the one with the smallest RTT. The existing single-sample test is unchanged. New tests: a slow pong does not move the offset, a faster pong takes over, and an old fast sample ages out.
|
|
11. **Partial chain.**
|
|
- `chainToText` numbers from `result.chainLength`: the opening word is always 1, and later words are numbered back from the total. It inserts a `…` line after the opening when words are missing (new string `exportGap`). Two tests cover a partial and a complete chain.
|
|
- Skipped: the optional "…" row in `ChainHistory` on screen. It was marked optional.
|
|
12. **Chat re-announce.** A `chatHistory` line matching an on-screen line on `(atMs, playerId, text)` keeps that line's ordinal, so its keyed row is not re-inserted into the live region. Identical duplicate lines each claim one old line. Two tests.
|
|
13. **WordInput tests.**
|
|
- Added: submit clears the field only when `onsubmit` returns true, and submit is refused during composition (and works after `compositionend`).
|
|
- Added: `beforeinput` is cancelled out of turn and not on the player's turn, and a composition's text is reverted out of turn.
|
|
- The vacuous composition test is rewritten as "leaves the player's own text alone on their turn". It fails if the revert ignores `enabled`, but its purpose is to pin the on-turn behaviour.
|
|
14. **Tests.**
|
|
- New `tests/status-components.test.js` covers `CountdownRing`, `PlayerStatus`, `GameOverPanel` and `ArmedButton`. Details are below.
|
|
- `ws-client.test.js` now covers storage that throws on read, on write, on remove, and a `sessionStorage` property that throws.
|
|
- New `tests/reactive-props.svelte.js` is a helper only, so a test can change a mounted component's props (the `$state` rune needs a `.svelte.js` module).
|
|
- `i18n.test.js` has a new test that scans `src/**` for `fill(t.key, {…})` calls and checks the object's keys equal the template's placeholders. It asserts more than 10 calls were matched, and it cannot see calls whose template is chosen by a conditional, which the test comment notes.
|
|
- Skipped: a `Lobby` component test. It was marked "if time allows", and Lobby is large and mostly wiring.
|
|
15. **`vite.config.js`.** The comment no longer says "two suites".
|
|
16. **`h1` on `/online`.** Now `var(--text-5)`, which is what the rules page `h1` uses. The size goes from 1.3rem to 1.5rem, a small visible change.
|
|
|
|
`status-components.test.js` contents:
|
|
- `CountdownRing`: idle dash, rounded-up seconds and label, urgent only on the player's own turn, stalled instead of urgent offline, and the spoken 10 s mark.
|
|
- `PlayerStatus`: banner structure, countdown to zero, banner removed on return.
|
|
- `GameOverPanel`: standings and rank classes, no table for a one-row result, focus taken, focus refused while typing elsewhere, rematch button only when supplied, and export producing a `.txt` download.
|
|
- `ArmedButton`: arm, confirm, timeout disarm, disarm on `disabled`.
|
|
|
|
## Other changes
|
|
|
|
- **Footer link (security review L5, footer part).** `data/ATTRIBUTION.md` (the nine-item modification list) is not served over HTTP by the server or the web build. `AttributionFooter` now links "xem danh sách thay đổi" to `https://github.com/tiennm99/noitu/blob/main/data/ATTRIBUTION.md`. I confirmed the file exists on `main`. Strings `attributionModified` and `attributionChanges` are in `vi.js`. The rules page was not changed.
|
|
- **New error message.** `errcodes.go` now has `unknown_message`, added concurrently by the server agent. It made `tests/error-codes.test.js` fail, so I added a Vietnamese message for it in `vi.js`: "Máy chủ không hiểu yêu cầu này. Hãy tải lại trang." Please confirm the wording.
|
|
|
|
## `.primary` consolidation
|
|
|
|
`app.css` now has a global `.primary`: `border-color: transparent`, `background: var(--accent)`, `color: var(--accent-text)`, `transition: background-color 150ms ease-out`, plus `:hover:not(:disabled)` to `--accent-hover`, `:active:not(:disabled)` to `--accent-pressed`, and `:disabled` to `--surface-alt` background and `--text-muted` colour. It does not use `:where()`. Focus-visible is unchanged, since the global `:where(...)` focus rule was never touched. Sizing, padding, radius, weight and border width stay local.
|
|
|
|
Rules removed:
|
|
- **`online/+page.svelte`:**
|
|
- `.primary` lost `background`, `color` and `transition`, keeping `min-height`, `padding`, `border: 0`, `border-radius` and `font-weight`.
|
|
- Removed `.primary:hover:not(:disabled)`, `.primary:active:not(:disabled)` and `.primary:disabled`.
|
|
- These were identical to the global ones.
|
|
- **`ChatPanel.svelte`:**
|
|
- `.row button` lost `background`, `color` and `transition`.
|
|
- Removed `.row button:hover:not(:disabled)`, `.row button:active:not(:disabled)` and `.row button:disabled`.
|
|
- The send button gained `class="primary"`.
|
|
- **`WordInput.svelte`:**
|
|
- `.input-row button` lost `background`, `color` and `transition`.
|
|
- Removed `.input-row button:hover:not(:disabled)`, `:active:not(:disabled)` and `:disabled`.
|
|
- The submit button gained `class="primary"`.
|
|
- The `.fix.suggestion` rules are untouched.
|
|
- **`+page.svelte` (landing):**
|
|
- Removed `.actions .primary`, `.actions .primary:hover` and `.actions .primary:active`.
|
|
- The old hover and active had no `:not(:disabled)`, but that button is never disabled.
|
|
- `.actions > *` kept size, padding, radius and weight. It no longer sets the border colour, background or `color: inherit`.
|
|
- A new `.actions > :where(:not(.primary))` sets `border-color`, `background: var(--surface)` and `color: inherit`.
|
|
- `border` was split into `border-width: 1px; border-style: solid`, so the shorthand's `currentcolor` cannot beat the global transparent.
|
|
- **`GameOverPanel.svelte`:**
|
|
- Removed `.actions .primary`, `.actions .primary:hover` and `.actions .primary:active`.
|
|
- `.actions button` kept its sizing and `border-width`/`border-style`. The border colour and `background: var(--surface)` moved to `.actions button:where(:not(.primary))`.
|
|
- `:where()` keeps that rule's specificity at (0,1,1), so `.actions .export` still overrides it as before.
|
|
|
|
I checked the compiled selectors of the landing page and GameOverPanel with the Svelte compiler.
|
|
|
|
The report claimed the local rules only restated the colours. For the landing page and GameOverPanel that was wrong: `.actions > *` and `.actions button` set a background that would have beaten the global class. The `:where(:not(.primary))` split above is the fix. Lobby is untouched.
|
|
|
|
## Needs a human visual check (no browser here)
|
|
|
|
- The four `.primary` surfaces: landing "Chơi với máy", `/online` quick match and create buttons, the chat send button, and the word submit button. Check rest, hover, press, disabled and the dark theme.
|
|
- The GameOverPanel action row: the rematch button is accent-filled, home is a bordered button, and the export button is transparent and muted.
|
|
- The `/online` `h1` at 1.5rem.
|
|
- The away banner: the seconds should sit inline after the sentence, with a space between them.
|
|
- The footer: the extra sentence and link should still fit on a phone width.
|
|
- Behaviour that needs a real reconnect, ideally an e2e run when a browser exists:
|
|
- Finding 5: kick or ready right after a socket cut.
|
|
- Finding 6: leave while offline.
|
|
- Finding 7: chat button disabled during a cut.
|
|
- Finding 4: reload `/play` mid-game.
|
|
- Finding 1 end to end, once the server change lands.
|
|
|
|
## Notes
|
|
|
|
- After a refused resume on `/play` the player sees an idle board with a banner and must use Home or reload. An automatic restart would be a product call.
|
|
- The server's replay for Finding 1 was not verified against the wsapi change, which is being written concurrently. The client relies only on the frame order given in the task.
|
|
|
|
Status: DONE_WITH_CONCERNS
|
|
Summary: All 16 findings, the `.primary` consolidation and the footer link are done in `web/`; check, lint and 326 tests pass, and each behavioural fix has a test that fails when reverted (Finding 1 needed no client change).
|
|
Concerns: The Finding 5 page logic, the `/play` `freshSession` call site and all CSS changes are unverified without a browser (list above). I added a Vietnamese string for the new server code `unknown_message`. I did not add an e2e spec or a Lobby component test.
|