121 lines
5.5 KiB
Markdown
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.
|