Add regression coverage for predict mode - #9
Conversation
Predict mode shipped without tests. This adds them, and fixes only what writing them exposed. To test the challenge logic without copying it, the pure half moves to lib/predict.ts: signatures, distractor construction, the diff classifier and the screen-reader description, all plain functions over plain data. lib/stepper.tsx keeps the state and rendering and imports them, so tests and production share one implementation. ArrayCell and ArrayFrame are declared there and re-exported, leaving every chapter's import untouched. buildChallenge now takes an optional shuffle so a test can pin the option order; production still defaults to Fisher-Yates over Math.random. Two defects the tests exposed: The score outlived the demo it was earned in. Chapters that let the learner switch case swap the frames prop, and only the pending question was cleared, so a score followed the learner into a walkthrough they had never answered. Score and question now reset together, keyed on frame contents rather than on frames.length (two demos can be the same length) or array identity (a chapter building frames inline would otherwise reset on every render). The scope is documented in CLAUDE.md: one score belongs to one dataset, and a language switch is not a dataset change. pick() read the open question from its closure, so a second dispatch arriving before the re-render could score the same question twice. The decision now happens inside the state updater. Coverage: 54 unit and component tests plus 4 Playwright specs. Layout lives in the E2E specs because jsdom has no layout engine and reports every width as 0 — the 360px case asserts the panel, each option and the feedback stay inside the viewport, that options stack into one column, and that any over-wide mini board is scrollable rather than clipped. The main E2E flow answers wrong, checks the verdict names the axis, reveals, answers right, checks the score, switches to Chinese and answers again to confirm the interaction survives it. CI runs typecheck, unit tests, build and end-to-end on every pull request. No predict mode was added to hand-built visualizations. CLAUDE.md now records which chapters have it: the nine that use ArrayStepper, and not binary-tree, graph, linked-list or trie, whose animations are bespoke components. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 30554d146d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| setScore((s) => ({ | ||
| right: s.right + (i === c.correct ? 1 : 0), | ||
| total: s.total + 1, | ||
| })); |
There was a problem hiding this comment.
Move score updates out of the challenge updater
When React invokes functional state updaters more than once—most visibly under development Strict Mode—both invocations receive the unanswered challenge and execute this nested setScore, so one click can increment the score twice. State updater functions must remain pure; compute the accepted answer once outside this updater or update the challenge and score together in a reducer.
Useful? React with 👍 / 👎.
Predict mode shipped without tests. This adds them, and fixes only what writing them exposed. No redesign, no new course content, no predict mode added to hand-built visualizations.
Making the logic testable
The pure half moves to
lib/predict.ts— signatures, distractor construction, the diff classifier, the screen-reader description — as plain functions over plain data.lib/stepper.tsxkeeps the state and rendering and imports them, so tests and production share one implementation; the challenge logic is never reimplemented in a spec.ArrayCell/ArrayFrameare declared there and re-exported, so all 15 chapters' imports are untouched.buildChallenge(frames, step, n, shuffle?)gains an optional shuffle so tests can pin the option order; production still defaults to Fisher-Yates overMath.random.Two defects the tests exposed
The score outlived the demo it was earned in. Chapters that let the learner switch demo case swap the
framesprop, and only the pending question was cleared — so a score followed the learner into a walkthrough they had never answered. Score and question now reset together, keyed on frame contents:frames.lengthis wrong because two demos can be the same length, and array identity is wrong because a chapter building frames inline would reset the score on every render. Both cases are pinned by tests.pick()could score one question twice. It read the open question from its closure, so a second dispatch arriving before the re-render would pass thepicked !== nullguard. The decision now happens inside the state updater.Score scope, as documented
One score belongs to one frame dataset. Switching demo resets it; switching language does not. Recorded in
CLAUDE.mdand next to the effect that implements it.Coverage
predict.test.ts— every step, and under three different shufflespredict.test.tspredict.test.tspredict.test.ts, incl. clamping and the shifted candidate order when there is no frame two steps aheadpredict.test.tsarray-stepper.test.tsx— options disabled, second pick ignoredarray-stepper.test.tsxpredict.test.ts+array-stepper.test.tsxarray-stepper.test.tsx,predict-mode.spec.tsarray-stepper.test.tsx,predict-mode.spec.tsarray-stepper.test.tsxarray-stepper.test.tsx— incl. same-length demo and rebuilt-but-equal datasetpredict.test.ts+array-stepper.test.tsx, plus a dataset shrinking under the current steppredict.test.ts+array-stepper.test.tsxpredict-mode.spec.ts54 unit/component tests + 4 Playwright specs. Layout assertions live in E2E deliberately: jsdom has no layout engine and reports every width as 0, so a 360px test there would pass no matter what. The narrow-viewport spec checks the panel, each option and the feedback stay inside the viewport, that options stack into one column, and that any over-wide mini board is scrollable rather than clipped.
E2E flow: enable Predict → Next → answer incorrectly → verify the verdict names the axis → reveal and continue → answer correctly → verify the score → switch to 中文 → answer again and confirm the panel, labels and score all still work.
Scope of predict mode, now documented
CLAUDE.mdrecords that predict mode comes withArrayStepperand therefore covers array, string, stack, queue, hash, bst, heap, union-find, advanced (23 steppers). binary-tree, graph, linked-list and trie animate through bespoke components inapp/<ch>/viz.tsxand have none — unchanged by this PR.CI
.github/workflows/ci.ymlruns typecheck → unit tests → production build → Playwright on every pull request, uploading the report if E2E fails.Locally:
npm run typecheckclean,npm test54/54 (stable over three consecutive runs),npm run build19/19 pages,npx playwright test4/4.🤖 Generated with Claude Code