Files
baya-monorepo/docs/rules/client/testing.md
T
2026-07-30 02:26:52 +03:30

121 lines
5.5 KiB
Markdown

# 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](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](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](theme.md) for the
color-scheme ones specifically, which fail *silently* rather than at compile time.
- Avoid deprecated MUI APIs that throw at runtime.