# 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//.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 ``** 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 ` 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.