Files
baya-monorepo/archive/docs/rules/client/testing.md
T
2026-08-02 20:01:31 +03:30

5.5 KiB

Client testing, lint and types

What is tested and how, plus the two gate tools and their traps.

Last verified: 2026-07-30 against commit d3ec723.


1. What is tested

Every shared component has a co-located test file. A component is "shared" if it is imported from more than one place — a page, a layout, or another component.

There are 125 test files across src/ (114 *.test.tsx, 11 *.test.ts). Location: src/components/<Name>/<Name>.test.tsx, next to the component.

Coverage baseline for a shared component

  1. It renders without crashing.
  2. Every documented prop produces the correct HTML attribute or CSS class.
  3. User interactions (click, change) call the expected callbacks.

That is a floor, not a ceiling. Where a component encodes a rule, test the rule: BookingDetailView's test proves the customer never fires the care-instructions query (the two-stage disclosure gate), and matchActivePath's test proves a nested route lights up its parent tab and never a sibling. Those are the tests worth writing.

The two rules about the test itself

  • Wrap with <ThemeProvider> if the component uses MUI theming.
  • Do NOT mock MUI components. Test against the rendered DOM. A test that mocks Button proves nothing about what a user sees.

Before removing or renaming a shared component

Check whether any src/**/*.test.{ts,tsx} imports it. If so, update or delete those tests in the same change. A dangling test import fails the suite, and a test left behind for a deleted component is dead code.

Deliberate coverage gaps

NeshanMap is not unit-tested — jsdom plus Leaflet is an integration problem, not a unit one — and it is unreachable in CI anyway, because NEXT_PUBLIC_NESHAN_KEY is unset there so AddressMapPicker falls back to the bounded-canvas stand-in. Both branches of that fork are tested; the map itself isn't.


2. Jest configuration

jest.config.ts replaces next/jest's transformIgnorePatterns array outright rather than appending to it. This is deliberate and easy to undo by accident:

next/jest's default pattern already broadly matches all of node_modules (its negative-lookahead allowlist carves out only a couple of Next-internal packages), and Jest's array semantics are OR-based — a file is ignored if any pattern matches. So appending a more permissive pattern can never "un-ignore" a package an earlier pattern already caught. The array has to be replaced, by post-processing the async config next/jest returns.

What it lets through: next-intl, use-intl, @formatjs, intl-messageformat — all ESM-only builds.

That fixes real imports; it does not make the dependency free. See components.md "Presentational purity" for why ErrorBoundary/ErrorState are caller-owned and Money is the one sanctioned exception.


3. The gate

npm run check   →  npm run type  &&  npm run lint  &&  npm run lint:copy

Plus npm run test:ci when you touched a component with a co-located test.

Both gate tools are plain CLI tools. There is no next lint — it was removed in Next 16, and calling it silently does nothing.

Script Runs
type tsc --noEmit. tsconfig.json: strict on, noEmit, @/*src/*
lint eslint ., driven by flat config in eslint.config.mjs
lint:copy node scripts/check-copy.mjs — see i18n.md §4

eslint.config.mjs spreads eslint-config-next (core-web-vitals + typescript + react + react-hooks + jsx-a11y + import) and applies eslint-config-prettier last, so ESLint never fights Prettier on formatting.


4. Lint rules for this project

  • Flat config only. Do not add .eslintrc* files — put any rule change in eslint.config.mjs.
  • ESLint owns correctness, Prettier owns formatting. Don't add stylistic ESLint rules.
  • No unused variables or imports. @typescript-eslint/no-unused-vars is raised from eslint-config-next's default warn to error, so dead code fails npm run check. Delete it rather than disabling the rule; prefix a deliberately-unused binding with _ (_event, catch (_err)) to opt out.
  • Prefer fixing code over silencing the linter. When a disable is genuinely correct — the real example here is a deliberate browser-only read after mount that trips react-hooks/set-state-in-effect — use a scoped // eslint-disable-next-line <rule> with a one-line reason. Never a file-wide disable.

Two pinned constraints

  • ESLint is pinned to 9. ESLint 10 currently crashes with this Next 16 toolchain (scopeManager.addGlobals is not a function). Don't bump it as a housekeeping change.
  • import/no-cycle is disabled — its TypeScript resolver has an interface mismatch here. The reason is noted in eslint.config.mjs; don't re-enable it without checking that note.

5. MUI v9 API only

The type gate catches most of this, but not all of it, and the failures are confusing when it doesn't.

  • Use sx={{ mb: 4 }}, not mb={4} as a direct prop.
  • Do not pass flexWrap or useFlexGap as direct props to Stack. Neither is a valid v9 Stack prop; both cause a TypeScript overload error. Use sx={{ flexWrap: 'wrap' }}. useFlexGap was a v5 opt-in and does not exist in v9.
  • No other v5/v6-era props: storageWindow, InitColorSchemeScript. See theme.md for the color-scheme ones specifically, which fail silently rather than at compile time.
  • Avoid deprecated MUI APIs that throw at runtime.