Testing#
CrabbyTab's tests are Vitest unit tests. They run in Node, need no browser, no Firebase project and no credentials, and are deterministic. This page covers how to run them, where they live, the differential harness against Tabbycat, the translation checks, and what is not covered.
Running the checks#
The scripts in package.json:
| Command | What it runs |
|---|---|
npm run test (or npm test) |
vitest run: every *.test.ts file once |
npm run lint |
next lint with .eslintrc.json (next/core-web-vitals) |
npm run build |
next build, the production build |
npm run i18n:check |
the translation catalogue tests (see below) |
npm run i18n:new -- <code> |
creates a new language catalogue from English |
Other useful commands (not scripts, run through npx):
npx vitest # watch mode
npx vitest run src/lib/draw # one folder
npx vitest run src/lib/ballots/result.test.ts # one file
npx vitest run -t "low-point" # tests whose name matches
npx tsc --noEmit # full type check, tests included
tsconfig.json includes every .ts and .tsx file, so tsc --noEmit type-checks the tests and the tools/
folder too.
Continuous integration#
.github/workflows/ci.yml
runs on every push and pull request, on Node 22:
npm ci → npm run lint → npx tsc --noEmit → npm run test → npm run build
All five must pass. CI needs no Firebase project, Docker or Python.
Setup#
vitest.config.ts:
environment: "node"(there is no DOM; nothing renders React),globals: true(describe,it,expectwithout imports, though most files import them fromvitest),- the
@alias points tosrc/, as in the app.
There is no include pattern, so Vitest's default applies: every *.test.ts file in the repository outside
node_modules, including tools/. There are about 250 test files.
Where tests live#
Tests sit next to the code they test, named <module>.test.ts. The main groups:
| Folder | Covers |
|---|---|
src/lib/draw/ |
draw generation: two-team and BP power pairing, brackets and pull-ups, conflict avoidance, minimum-cost and blossom matching, side allocation, round robin, eliminations, N teams per room, venue and adjudicator allocation, seeded reproducibility |
src/lib/standings/, src/lib/breakqual/ |
standings, precedence and tie handling, break qualification rules |
src/lib/ballots/, src/lib/scoring/ |
ballot validation and aggregation, winner rules, voting and individual ballots |
src/lib/csv/, src/lib/participants/, src/lib/registration/ |
CSV import and export, participant validation, registration |
src/lib/publicView/, src/lib/seo/ |
the public projection, release rules, caching, sitemap and robots rules |
src/lib/email/, src/lib/api/v1/ |
email recipients and templates, the REST API |
src/lib/*.test.ts |
private URLs and passcodes, rate limits, staff permissions, tournament access, audit chain, security headers, Firestore rule text (below) |
src/features/tournament/application/ |
the application commands: draws and regeneration, ballots and confirmation, breaks, round completion, live sync, cloud sync and conflicts, backups, DebateXML import and export, whole-tournament lifecycles (lifecycle.bp.test.ts, lifecycle.wsdc.test.ts) |
src/features/tournament/infrastructure/ |
Firestore repositories against an in-memory fake, including uploadScale.test.ts (a synthetic 1000-team upload) |
src/app/api/**/ |
a few route handlers, with Firebase Admin mocked |
src/lib/i18n/, tools/i18n/ |
translations |
tools/tabbycat-diff/ |
the differential test against Tabbycat |
Sample CSV files for manual testing of the import pages are in testdata/ (institutions, teams, adjudicators,
venues, motions). No automated test uses them.
Writing tests#
AGENTS.md section 11 sets the expectations. In practice:
- Test pure functions and application commands with explicit inputs. Keep tournament logic out of React components so it can be tested here.
- Mock external services. Commands take a repository or dependencies object; tests pass in-memory fakes or
vi.mockthe Firebase modules. Never require a live Firebase project. - For anything random, pass a fixed seed and assert that the same inputs and seed give the same output.
- Test invalid and infeasible cases, not only the happy path: no feasible draw, side limits, duplicate records, missing results (not zero), malformed CSV, quoted fields and embedded newlines.
- Keep tests independent of execution order and of the clock; inject timestamps.
- When a test pins a Tabbycat behaviour, say so in the test name or a comment and point at the Tabbycat source.
Differential testing against Tabbycat#
tools/tabbycat-diff/ runs the same tournaments through the official
Tabbycat (v2.12.1) and through CrabbyTab's own libraries, and compares
the results rule by rule. Full documentation:
tools/tabbycat-diff/README.md;
latest findings:
report/REPORT.md.
What it compares#
For each fixture, tabbycatDiff.test.ts imports the fixture's DebateXML with CrabbyTab's importer, applies
Tabbycat's effective preferences (from the golden file), and computes through the real libraries:
| Area | Compared |
|---|---|
| Standings | every team and speaker metric both engines compute, ranks and the reply tab, after every round. Tabbycat's null (no results) never equals a CrabbyTab 0. |
| Draws | a draw for every round from the rounds before it, over eight seeds: room count, coverage, bracket composition, pull-ups, side imbalance, institution clashes, repeat pairings, side-cap violations, BP position cost and byes. Draws often have several valid answers, so they are compared by these properties, not room by room. |
| Breaks | breaking and reserve teams per category, in priority order, and break ranks |
| Adjudicator allocation | hard conflicts, chairs, panel sizes, history repeats and the sum of chair scores, using CrabbyTab's ports of Tabbycat's Voting and Consensus Hungarian allocators |
Every discrepancy must have a human verdict in known-differences.json: rule difference, CrabbyTab defect,
configuration difference or valid alternative. The test fails when a discrepancy has no verdict, or when the
set of discrepancies or verdicts differs from the committed report/report.json (something was fixed or
something regressed). It also fails if a fixture has no golden file.
Two smaller tests sit beside it: compare.test.ts (the comparison rules themselves) and tieOrder.test.ts
(given Tabbycat's fixed tie order, CrabbyTab pairs exactly Tabbycat's rooms and byes in the twoteam-odd-ties
fixture).
Not compared: elimination rounds, feedback, motions and venues. Debate importance is 0 on both sides.
Fixtures and golden files#
| Path | What it is |
|---|---|
fixtures/<name>/tournament.xml, config.json |
the tournament (DebateXML) plus what DebateXML cannot carry |
fixtures/generate.ts |
the seeded generator that writes the fixtures |
golden/<name>.tabbycat.json |
Tabbycat's output, committed so CI needs no Docker or Python |
tabbycat/ |
the Tabbycat runner (run_tabbycat.py), Dockerfile, docker-compose.yml, run-native.sh |
The three fixtures are bp-uneven (BP, 26 teams with byes, three break categories, iron-person speeches),
australs-voting (Australs two-team rules, voting panels with splits, replies) and twoteam-odd-ties (19 teams,
wins only, many ties).
Regenerating#
All commands run from the repository root. In order:
Fixtures (only after editing
FIXTURESinfixtures/generate.ts):node tools/tabbycat-diff/fixtures/generate.tsThis runs a TypeScript file directly, so it needs a Node version with type stripping on by default (22.18 or later). The output is byte-identical for the same seed. Node prints a harmless
MODULE_TYPELESS_PACKAGE_JSONwarning.Golden files (Tabbycat's side; after changing fixtures or the Tabbycat version). With Docker:
docker compose -f tools/tabbycat-diff/tabbycat/docker-compose.yml run --rm --build tabbycat docker compose -f tools/tabbycat-diff/tabbycat/docker-compose.yml down -vWithout Docker you need Python 3.11, git and the PostgreSQL binaries:
PG_BIN="/c/Program Files/PostgreSQL/18/bin" tools/tabbycat-diff/tabbycat/run-native.shThe native script clones Tabbycat into
~/.cache/crabbytab-tabbycat-diff(override withTABBYCAT_DIFF_WORK) and runs a throwaway local PostgreSQL cluster on port 54329, deleted afterwards. The first run takes about seven minutes. The committed golden files came from the native route; the Docker route has not been run yet.Report (after reviewing a change in the discrepancies, and adding verdicts for new ones to
known-differences.json):TABBYCAT_DIFF_WRITE=1 npx vitest run tools/tabbycat-diffThis rewrites
report/REPORT.mdandreport/report.json. Do not edit them by hand. On PowerShell, set the variable first:$env:TABBYCAT_DIFF_WRITE = "1"; npx vitest run tools/tabbycat-diff.
None of these is an npm script.
Translation checks#
npm run i18n:check runs vitest run src/lib/i18n:
catalogues.test.ts: every catalogue has every English key and no unknown keys, every plural category the language needs, and the same{placeholders}and<tags>as English;config.test.ts: locale matching, selection order, text direction and the pre-paint script;translate.test.ts: loading, fallback, interpolation, plurals, rich text and formatting.
The same tests run in npm test, so adding an English string without adding the key to every other catalogue
fails CI. npm run i18n:new -- <code> writes src/lib/i18n/messages/locales/<code>.json through
tools/i18n/newLanguage.test.ts; in a normal test run that file only checks the template builder. See
Languages and translations.
Firestore rules#
There is no Firestore emulator setup and the rules are not executed in CI (the emulator needs Java and
firebase-tools). Two tests read firestore.rules as text instead:
src/lib/liveRules.test.tskeeps the field lists data-entry staff may write directly (live ballots, check-ins) equal toASSISTANT_LIVE_BALLOT_FIELDS/ASSISTANT_LIVE_CHECK_IN_FIELDSinsrc/lib/dataEntrySync.ts;src/lib/serverOnlyRules.test.tschecks that theserverOnlynamespace (invitations, registration, custom permission groups) is excluded from the administrator rule and denied to every client.
Before deploying rule changes, check them by hand in the Firebase console's Rules Playground with the cases listed in live-collaboration.md.
Not covered#
- No React component tests (the environment is Node, and no DOM testing library is installed).
- No end-to-end browser tests. Check UI changes by hand with
npm run dev. - No executed Firestore rules tests (see above).
- Tabbycat differential coverage is limited to the three fixtures and the areas listed above.
A passing build and test run do not prove tournament logic is correct or fair; add a test for the rule you changed.