Appearance
Test Quality
What a tile's tests must establish. unit-tests.md and e2e-tests.md cover the mechanics — the harness, waiting, fixtures, what runs where. This file covers whether a test is worth its line.
- Coverage tells you that a line ran, not that anything checked what it did. A tile's rule module can sit at 90% line coverage, with a test named for every rule in it, and still pass after each of those rules is deleted one at a time — because a deleted rule leaves the line executing, just wrongly. Test counts, one-per-UI-state floors, and coverage thresholds are all volume measures, and volume is not the thing that catches a regression. Everything below is about what a single test distinguishes, and it is in addition to the coverage thresholds a project sets, never instead of them — a suite still has to meet its gate, it just cannot stop there.
- A test must fail when the behavior it names is broken. Check that; do not assume it. After writing a test for a rule, delete or invert that rule in the source, run the test, and confirm it fails — then restore the rule. This takes seconds, it is the only way to learn that an assertion is load-bearing, and it routinely exposes a test that passes for a reason unrelated to its name. Do this for each rule that decides whether a player's action is legal, whether a round is over, or who is allowed to act.
- Assert which rule rejected an action, not merely that something did. A test that only checks the action failed keeps passing when the rule it was written for is deleted, because a neighbouring rule usually rejects the same input for its own reason: a square several cells away is both out of order and not adjacent, so a single shared "invalid move" message cannot tell those apart. Two things follow. Return the cause rather than a boolean — a validator typed
boolean, or one that throws the same message for every violation, makes the precise assertion impossible to write, so give each rule a distinct reason code or message. And choose an input that violates only the rule under test, so the assertion cannot be satisfied by a different rule firing first. - Test generated content as a pure function, over many seeds. When the tile generates a board, level, deal, or daily puzzle, keep generation in a module that touches neither the store nor the DOM so a test can call it directly. One valid output is not a passing grade: assert the invariants that make the content playable at all — solvable, fully reachable, inside the bounds the rules assume — across many seeds rather than one, and assert that different seeds produce different content. A generator that stops reading its seed still returns a perfectly valid board, so every single-seed test keeps passing while every player gets the same content forever. If the tile varies difficulty, assert that the variation actually happens.
- Prefer one test per rule over one test per screen. A test that walks a whole successful session through the real code proves the happy path works and little else; when several rules are exercised in one pass, any of them can be deleted without the test noticing. Scenario tests are still worth having for wiring and multi-client behaviour — but each rule that can reject, end, or gate something needs its own case, named for that rule.
- Sound, generated content, and anything the player cannot see in a screenshot need a behavioural test, because no visual pass will ever catch them. A cue that stopped firing, a board that stopped varying, and a preference that quietly bumped shared state all look identical to a correct tile in a screenshot.