Testing
A correctness-first priority order for a financial app, and what CI actually gates.
Stack: Vitest + @testing-library/react + jsdom. globals: true, so describe / it / expect are ambient. Setup in vitest.setup.ts loads @testing-library/jest-dom/vitest.
Priority order
This is a financial app, so the order is correctness-first, not coverage-first.
Pure functions — always
Formatters, trading math (math.ts, order-math.ts, orderbook.ts), slug helpers, mappers,
address truncation. These are where a wrong answer becomes a wrong trade.
Stores
Zustand transitions and selectors. setState to seed, getState to assert, reset in
beforeEach.
Critical hooks
The ones combining stores with side effects: use-resize, use-format, use-pip,
use-trades-pagination, use-stream-multi. Test the public output, not the internals.
Components — selectively
Only where there is non-trivial logic: form validation, multi-branch rendering. Do not test "renders without crashing".
E2E — not yet
Playwright golden paths (connect → place → see in positions) land once the write flow is stable.
Round-trip over point assertions
For anything invertible, assert the property rather than a handful of examples:
it('parse(format(x)) === x across the grid', () => {
for (const x of SAMPLES) {
expect(parseSize(formatSize(x, szDecimals), szDecimals)).toBe(x);
}
});Point assertions on formatters pass right up until someone adds locale grouping. A round-trip catches it, because grouping breaks the inverse.
Grid tests are the high-value ones
The engine rejects rather than rounds, so the client-side grid is the last line of defence against a rejected order that looked valid in the form:
it('floors size onto the grid before deriving margin', () => {
const snapped = snapSize('0.123456789', 5); // szDecimals = 5
expect(snapped).toBe('0.12345');
expect(marginFor(snapped, price, leverage)).toMatchInlineSnapshot();
});
it('exempts reduce-only from minNotional', () => {
expect(validate({ reduceOnly: true, notional: 1 })).toEqual({ ok: true });
});Layout
Tests are co-located with a .test.ts(x) suffix; some ui/ primitives use a __tests__/
folder.
CI and hooks
| Gate | Runs |
|---|---|
.github/workflows/ci.yml | lint → test:run → build on push to develop / next / main, and every PR |
Husky pre-commit | lint-staged — ESLint --fix on staged .ts(x) |
Husky pre-push | tsc -b && npm run test:run |
CI does not gate Cloudflare Pages deploys — CF watches the same branches independently. A red
build does not stop a deploy. Treat the pre-push hook as the real gate, and --no-verify as
something you justify out loud.