This site uses one functional cookie to keep feature rollouts consistent for you. Nothing is set until you choose. See the privacy notice.
Dev notes
3499+ tests (3432 unit + 67 e2e) — Vitest, Testing Library, MSW, and Playwright. Pure functions first, then hooks with fetch mocking, with a specific technique for proving that optimistic updates actually fire before the server responds.
One principle that should be followed and be non-negotiable is Test-Driven Development (TDD). This codebase, before the change, had zero test files. The goal was to close that gap by targeting the parts of the codebase that are actually worth testing: the rate limiter, the calendar layout algorithm, the vitals formatting, and the hooks with their optimistic update behavior.
renderHook and waitFor — both are needed for anything that touches React QueryonUnhandledRequest: "error" so any fetch without a registered handler fails the test immediately instead of silently passingnext/server and Auth0 and require a Next.js runtimeThe first attempt at testing optimistic updates used a fetchCount variable to make the GET handler return different data on first vs subsequent calls. It worked, but it was testing the wrong thing — it verified the end state after a completed mutation cycle, not whether the cache updated before the server responded.
The fix is delay() from MSW. Add a 300ms pause to the POST/PUT/DELETE handler, then fire the mutation without awaiting it. The mutation is still in-flight when the assertion runs, so the test is checking the actual optimistic state — what the user sees before the server has responded.
The calendar's layoutDayEvents function takes a list of same-day timed events and assigns each one a column index and a total column count, so they can be absolutely positioned side by side in the time grid. The tests cover: a single event gets column 0 of 1, two non-overlapping events share column 0, two overlapping events get separate columns, three simultaneous events get three columns each reporting totalColumns: 3, and pixel positions derived from the row height constant. This is the kind of logic that looks simple but has enough edge cases to be worth specifying in tests.
Tests only matter if they run automatically. A GitHub Actions workflow runs typecheck and the full unit suite on every push to main and develop, and on every PR targeting main. A second job runs after the first passes: it installs Chromium, boots the dev server, and runs the public Playwright suite — which includes axe-core accessibility scans on the landing page, TCG browser, and card detail page. A WCAG 2.1 AA violation blocks the merge just like a failing unit test. Vercel deploys are gated on both checks passing.
Every test uses factory functions instead of shared fixtures. makeEvent(), makeCountdown(), and makePage() accept optional overrides and return fresh objects with randomized IDs. No let declarations, no beforeEach mutations. Each test constructs exactly what it needs and nothing is shared between tests.
Update — August 16, 2026
The section above ends on the line that tests only matter if they run automatically, and I believed it enough to gate this app’s deploys on it. The design system repo, which publishes packages to npm the moment anything reaches its main branch, had a suite spread across four workspaces — the palette contrast gate, the component contrast pairs, the token shape checks — and no workflow that ran any of it. Its workflows directory held a Chromatic job, a publish job and a tag job, and nothing else.
And every one of them was green because I had remembered to run it. That is not a small distinction dressed up as a large one. A suite nothing runs is documentation of an intent, and it degrades exactly like documentation does: silently, at whatever rate the code moves, with no signal at the moment it stops being true. This one happened to still pass when I finally pointed a pipeline at it, which is luck rather than evidence.
Adding the workflow was ten minutes. The part worth keeping is what I did before merging it: a new gate that has only ever been green has been shown to run, not shown to fail, and those look identical in a checks list. So I reintroduced a regression the suite is meant to catch — reverting one chart palette slot to a colour that collides under deuteranopia — watched it exit non-zero with the ratio it objected to, and reverted the revert. A gate I have never seen fail is a gate I have no reason to trust.
The same repo also had a test count that doubled after a build, because its tokens package compiled its own tests into an output directory that the runner did not exclude, and then published that directory. The pipeline gap and the count are two of four findings of the same shape that week, written up in green checks.
Update — October 3, 2026
The line at the bottom of this page said there was no mutation testing, so the suite proved the tests run rather than that they would fail on a real break. I added Stryker and pointed it at the NFL live-projection math: the share of a game left on the clock, the scoreboard parser, and the win-probability model. It’s pure arithmetic full of boundaries, the kind of code where a test that only checks typical values passes against broken logic. And it hadn’t been checked against a live game yet, so the tests were all it had.
Stryker makes small edits to the code (flip a comparison, drop a condition, swap Math.max forMath.min), runs the tests against each one, and counts the edits the tests failed to notice. Of 191 mutants, 28 survived and 12 ran code no test touched.
File | % score | killed | survived | no cov games.ts | 83.33 | 35 | 6 | 1 matchups.ts | 75.37 | 98 | 22 | 11 winProbability.ts | 100.00 | 15 | 0 | 0 Final mutation score 79.06 under breaking threshold 80
Deleting if (period > 4) return 0, the overtime check in the game-clock share, changed nothing. An OT clock never runs longer than a quarter, so (4 − period) quarters plus the clock is already zero or negative from period 5 on, and the clamp after it returns 0 anyway. The check only looked like it was doing the work, so I deleted it and pinned the clamp with a 15-minute playoff OT test.
Replacing the week check in the stat lookup with truealso survived. Every fixture held a single week’s stats, so nothing proved the parser ignores ESPN’s season-total line that sits right beside the weekly one. Picking that up would show a 300-point projection for one game. One fixture with the season line first killed it.
[Survived] ConditionalExpression src/lib/nfl/matchups.ts:99 - (s) => s.scoringPeriodId === week && s.statSourceId === statSourceId, + (s) => true && s.statSourceId === statSourceId,
The parser’s “degrades on a payload that isn't a league” test was hollow in the same way. It passed{ nope: true }, which every optional field in the schema accepts, so the failure branch never ran. Anull payload exercises it.
Emptying out playerLine’s whole body, which should break every roster, “survived”, and Stryker flagged it as static. Applying the mutant by hand showed why. Adescribe block parsed its fixture at collection time, outside any test, so with the parser broken the suite crashed before a single test ran. Vitest reported that as no tests, and Stryker read no failures as a survivor.
FAIL src/lib/nfl/matchups.test.ts TypeError: Cannot read properties of undefined (reading 'started') ❯ src/lib/nfl/matchups.test.ts:313:17 Tests no tests
A real regression there would have looked like a suite that didn't run, not one that failed. Building the fixture inside a factory each test calls fixed it, which is also what the factory-function rule further up this page already asked for.
A few mutants change nothing observable. Replacing an absent list with a list of junk parses to nothing either way, because the schema skips every entry. Those get a disable comment with the reason, rather than a test that pins trivia or a higher threshold that hides them.
// Stryker disable next-line ArrayDeclaration: a missing list and a list of junk // both parse to nothing, so the default can't be observed.
The run now ends at 100%: 177 killed and none surviving, in 36 seconds. The break threshold went up to 90, so a regression fails without one new mutant tripping it. A separate CI job runs Stryker whenever that code, its tests or the config change, and uploads the HTML report even when it fails.