Skip to content

Add regression coverage for predict mode - #9

Merged
renrenmimi merged 1 commit into
mainfrom
test/predict-mode-regression-coverage
Aug 31, 2026
Merged

Add regression coverage for predict mode#9
renrenmimi merged 1 commit into
mainfrom
test/predict-mode-regression-coverage

Conversation

@renrenmimi

Copy link
Copy Markdown
Owner

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.tsx keeps the state and rendering and imports them, so tests and production share one implementation; the challenge logic is never reimplemented in a spec. ArrayCell / ArrayFrame are 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 over Math.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 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: frames.length is 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 the picked !== null guard. 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.md and next to the effect that implements it.

Coverage

Requirement Where
1 correct option is the real next frame predict.test.ts — every step, and under three different shuffles
2 no duplicate options predict.test.ts
3 no distractor shares the answer's signature predict.test.ts
4 pointer-only / cell-only / off-by-one built correctly predict.test.ts, incl. clamping and the shifted candidate order when there is no frame two steps ahead
5 deterministic order for an injected shuffle predict.test.ts
6 answerable only once array-stepper.test.tsx — options disabled, second pick ignored
7 score increments exactly once array-stepper.test.tsx
8 wrong answers identify cells / pointers / both predict.test.ts + array-stepper.test.tsx
9 predict disables autoplay array-stepper.test.tsx, predict-mode.spec.ts
10 back / replay clears an unfinished question array-stepper.test.tsx, predict-mode.spec.ts
11 changing dataset clears stale questions array-stepper.test.tsx
12 changing dataset clears a misleading score array-stepper.test.tsx — incl. same-length demo and rebuilt-but-equal dataset
13 empty and one-frame datasets do not crash predict.test.ts + array-stepper.test.tsx, plus a dataset shrinking under the current step
14 screen-reader labels describe cells and pointers predict.test.ts + array-stepper.test.tsx
15 no overflow at 360px predict-mode.spec.ts

54 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.md records that predict mode comes with ArrayStepper and 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 in app/<ch>/viz.tsx and have none — unchanged by this PR.

CI

.github/workflows/ci.yml runs typecheck → unit tests → production build → Playwright on every pull request, uploading the report if E2E fails.

Locally: npm run typecheck clean, npm test 54/54 (stable over three consecutive runs), npm run build 19/19 pages, npx playwright test 4/4.

🤖 Generated with Claude Code

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>
@vercel

vercel Bot commented Aug 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
data-data Ready Ready Preview Aug 31, 2026 11:37pm UTC

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread lib/stepper.tsx
Comment on lines +275 to +278
setScore((s) => ({
right: s.right + (i === c.correct ? 1 : 0),
total: s.total + 1,
}));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@renrenmimi
renrenmimi merged commit a7601a0 into main Aug 31, 2026
3 checks passed
@renrenmimi
renrenmimi deleted the test/predict-mode-regression-coverage branch August 31, 2026 23:39
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