|
| 1 | +# Code Quality |
| 2 | + |
| 3 | +> **Prerequisite:** Review and apply the common guidelines in [`common.md`](../common.md) before using this checklist. |
| 4 | +
|
| 5 | +## Arrange / Act / Assert (AAA) structure |
| 6 | + |
| 7 | +**Urgency:** suggestion |
| 8 | + |
| 9 | +### Category |
| 10 | + |
| 11 | +Style |
| 12 | + |
| 13 | +### Confidence Threshold |
| 14 | + |
| 15 | +Flag only when the test is long enough or complex enough that structure affects readability (for example, multi-step setup/interactions or >10 lines). For short, self-evident tests, prefer a suggestion or no comment. |
| 16 | + |
| 17 | +### Exceptions / False Positives |
| 18 | + |
| 19 | +- Do not flag short tests (roughly 3-5 lines) where Arrange/Act/Assert is obvious without comments. |
| 20 | +- Do not require AAA comments in parameterized tests when added comments make the test harder to scan than the code itself. |
| 21 | +- Prefer suggestions over high-severity findings unless the repository explicitly enforces AAA comment structure in tests. |
| 22 | + |
| 23 | +### Rules |
| 24 | + |
| 25 | +1. **Each section must be preceded by a comment** — `// Arrange`, `// Act`, and `// Assert`. |
| 26 | +2. **The Assert comment must include a brief explanation** of how the described scenario is being verified. The explanation should summarize what the assertions prove about the behavior stated in the test name. |
| 27 | + - Good: `// Assert - textarea shows hash subjects, participantId query param is ignored` |
| 28 | + - Good: `// Assert - ID Search button is active` |
| 29 | + - Bad: `// Assert` (missing explanation) |
| 30 | + - Bad: `// Assert - check results` (too vague; does not connect to the scenario) |
| 31 | +3. **Arrange may be omitted** when there is no setup beyond what `beforeEach` already provides, but Act and Assert are always required. |
| 32 | +4. **Combined `// Act & Assert` is acceptable** only when the assertion must be wrapped inside a `waitFor` that is inseparable from the action (e.g., awaiting an async side-effect). In that case the comment must still include an explanation: |
| 33 | + - Good: `// Act & Assert - empty state message is shown and error was logged to console` |
| 34 | + - Bad: `// Act & Assert` |
| 35 | +5. **Multiple Act/Assert cycles in one test are allowed** for stateful interaction flows (e.g., click then verify, click again then verify). Each cycle must carry its own `// Act` and `// Assert - ...` comments. |
| 36 | + |
| 37 | +--- |
| 38 | + |
| 39 | +## No unused variables, imports, or mocks |
| 40 | + |
| 41 | +**Urgency:** urgent |
| 42 | + |
| 43 | +### Category |
| 44 | + |
| 45 | +Maintainability |
| 46 | + |
| 47 | +### Confidence Threshold |
| 48 | + |
| 49 | +Flag when the symbol is clearly unused in the file or when a mock setup is not consumed by behavior under test. If a mock or import may be used implicitly (module mocking side effects, global setup conventions), verify before commenting. |
| 50 | + |
| 51 | +### Exceptions / False Positives |
| 52 | + |
| 53 | +- Do not flag side-effect imports (for example, polyfills or test environment setup) just because no identifier is referenced. |
| 54 | +- Do not flag module-level `jest.mock(...)` declarations that intentionally replace imports used elsewhere in the file. |
| 55 | +- If a placeholder parameter is required for function signature/position, allow underscore-prefixed names (for example, `_arg`). |
| 56 | + |
| 57 | +### Rules |
| 58 | + |
| 59 | +1. **Unused imports must be removed.** If a symbol is imported but never referenced in the file, delete the import. This includes named imports, default imports, and type-only imports. |
| 60 | +2. **Unused variables and constants must be removed.** If a `const`, `let`, or destructured binding is declared but never read, delete it. This applies to top-level declarations, inside `describe`/`beforeEach`/`test` blocks, and helper functions. |
| 61 | +3. **Unused mock declarations must be removed.** If `jest.fn()`, `jest.mock()`, `jest.spyOn()`, or a manual mock variable is set up but never referenced in an assertion or as a dependency, delete it. Mocks that are called implicitly (e.g., module-level `jest.mock('...')` that replaces an import used elsewhere) are considered used. |
| 62 | +4. **Unused mock return values must be removed.** If a mock is configured with `.mockReturnValue()`, `.mockResolvedValue()`, or `.mockImplementation()` but the return value is never consumed or asserted on, simplify or remove the configuration. |
| 63 | +5. **Unused helper functions and factory functions must be removed.** If a test utility, builder, or factory function defined in the file is never called, delete it. |
| 64 | +6. **Unused parameters in callbacks must be prefixed with `_`.** If a callback parameter (e.g., in `.mockImplementation((unusedArg) => ...)`) is required for positional reasons but not used, prefix it with `_` to signal intent (e.g., `_unusedArg`). |
| 65 | +7. **Unused `render` results must not be destructured.** If `render(<Component />)` is called and the return value is not used, do not destructure it. Write `render(<Component />);` instead of `const { container } = render(<Component />);` when `container` is never referenced. |
| 66 | + |
| 67 | +### Examples |
| 68 | + |
| 69 | +```tsx |
| 70 | +// ---- Unused imports ---- |
| 71 | + |
| 72 | +// ❌ BAD — ApiResponse is imported but never used |
| 73 | +import { render, screen } from '@testing-library/react'; |
| 74 | +import { ApiResponse, UserProfile } from '../models'; |
| 75 | + |
| 76 | +const mockProfile: UserProfile = { name: 'Test' }; |
| 77 | + |
| 78 | +// ✅ GOOD — only referenced imports remain |
| 79 | +import { render, screen } from '@testing-library/react'; |
| 80 | +import { UserProfile } from '../models'; |
| 81 | + |
| 82 | +const mockProfile: UserProfile = { name: 'Test' }; |
| 83 | + |
| 84 | + |
| 85 | +// ---- Unused variables ---- |
| 86 | + |
| 87 | +// ❌ BAD — mockHandler is declared but never used |
| 88 | +const mockHandler = jest.fn(); |
| 89 | +const mockCallback = jest.fn(); |
| 90 | + |
| 91 | +test('calls callback on click', () => { |
| 92 | + render(<Button onClick={mockCallback} />); |
| 93 | + fireEvent.click(screen.getByRole('button')); |
| 94 | + expect(mockCallback).toHaveBeenCalledTimes(1); |
| 95 | +}); |
| 96 | + |
| 97 | +// ✅ GOOD — only mockCallback remains |
| 98 | +const mockCallback = jest.fn(); |
| 99 | + |
| 100 | +test('calls callback on click', () => { |
| 101 | + render(<Button onClick={mockCallback} />); |
| 102 | + fireEvent.click(screen.getByRole('button')); |
| 103 | + expect(mockCallback).toHaveBeenCalledTimes(1); |
| 104 | +}); |
| 105 | + |
| 106 | + |
| 107 | +// ---- Unused mock setup ---- |
| 108 | + |
| 109 | +// ❌ BAD — jest.spyOn for console.warn is set up but never asserted or needed |
| 110 | +jest.spyOn(console, 'warn').mockImplementation(() => {}); |
| 111 | +jest.spyOn(console, 'error').mockImplementation(() => {}); |
| 112 | + |
| 113 | +test('renders error state', () => { |
| 114 | + render(<ErrorBanner message="fail" />); |
| 115 | + expect(screen.getByText('fail')).toBeInTheDocument(); |
| 116 | + expect(console.error).toHaveBeenCalled(); |
| 117 | + // console.warn is never checked |
| 118 | +}); |
| 119 | + |
| 120 | +// ✅ GOOD — only the console.error spy remains |
| 121 | +jest.spyOn(console, 'error').mockImplementation(() => {}); |
| 122 | + |
| 123 | +test('renders error state', () => { |
| 124 | + render(<ErrorBanner message="fail" />); |
| 125 | + expect(screen.getByText('fail')).toBeInTheDocument(); |
| 126 | + expect(console.error).toHaveBeenCalled(); |
| 127 | +}); |
| 128 | + |
| 129 | + |
| 130 | +// ---- Unused render destructuring ---- |
| 131 | + |
| 132 | +// ❌ BAD — container is destructured but never referenced |
| 133 | +const { container } = render(<Header title="Hello" />); |
| 134 | +expect(screen.getByText('Hello')).toBeInTheDocument(); |
| 135 | + |
| 136 | +// ✅ GOOD — render called without unused destructuring |
| 137 | +render(<Header title="Hello" />); |
| 138 | +expect(screen.getByText('Hello')).toBeInTheDocument(); |
| 139 | + |
| 140 | + |
| 141 | +// ---- Unused callback parameter ---- |
| 142 | + |
| 143 | +// ❌ BAD — `req` is not used but has no underscore prefix |
| 144 | +mockFetch.mockImplementation((req, options) => { |
| 145 | + return Promise.resolve({ ok: true }); |
| 146 | +}); |
| 147 | + |
| 148 | +// ✅ GOOD — unused positional parameter prefixed with _ |
| 149 | +mockFetch.mockImplementation((_req, options) => { |
| 150 | + return Promise.resolve({ ok: true }); |
| 151 | +}); |
| 152 | +``` |
| 153 | + |
| 154 | +--- |
| 155 | + |
| 156 | +## Async React updates must be awaited (`act` warning prevention) |
| 157 | + |
| 158 | +**Urgency:** urgent |
| 159 | + |
| 160 | +### Category |
| 161 | + |
| 162 | +Correctness |
| 163 | + |
| 164 | +### Confidence Threshold |
| 165 | + |
| 166 | +Flag as a high-severity finding when the changed test triggers async React updates and asserts before the UI settles, or when `act(...)` warnings are present in test output. If the interaction and state updates are fully synchronous, do not force async patterns. |
| 167 | + |
| 168 | +### Exceptions / False Positives |
| 169 | + |
| 170 | +- Do not require `await` for purely synchronous interactions that do not trigger async state updates. |
| 171 | +- If the test uses a project helper that already waits for async UI stabilization, avoid duplicating `waitFor`/`findBy` calls. |
| 172 | +- When fake timers are used, accept explicit `act(...)` + timer advancement patterns that correctly flush updates. |
| 173 | + |
| 174 | +### Rules |
| 175 | + |
| 176 | +1. **If a component triggers async state updates after render, the test must await a UI-stable condition before assertions.** Look for `useEffect` with async work, fetch-on-mount patterns, or promise-based state transitions. |
| 177 | +2. **User interactions that trigger async updates must be awaited.** `userEvent.click`, `userEvent.type`, and similar calls should be `await`ed when the resulting handler performs async work. |
| 178 | +3. **Tests with async UI behavior must be marked `async`.** A test function that uses `await` must be declared `async`; a test whose component performs async work almost certainly needs both. |
| 179 | +4. **Presence of `act(...)` warnings in test output is a defect, even when tests pass.** Treat these warnings as test failures during review. |
| 180 | + |
| 181 | +### Heuristics for detection |
| 182 | + |
| 183 | +- `render(...)` followed by immediate assertions while the component has `useEffect(() => { fetch(...).then(...) }, [])` or similar async-on-mount logic. |
| 184 | +- `userEvent.click/type/...` followed by immediate assertions that depend on async updates. |
| 185 | +- Presence of `console.error` `act(...)` warnings in test output, even when tests pass. |
| 186 | +- Test functions not marked `async` despite the component performing async work. |
| 187 | + |
| 188 | +### Preferred fixes (in order of preference) |
| 189 | + |
| 190 | +1. **`await screen.findBy...(...)` — preferred.** Use for elements that appear after async work. Simpler and more idiomatic than `waitFor`. |
| 191 | +2. **`await waitFor(() => expect(...))`** — use when asserting on state-dependent conditions that `findBy` can't express (e.g., element attribute changes, disappearance). |
| 192 | +3. **Shared helpers** like `waitForComponentToLoad()` — call after render when available. |
| 193 | +4. **`await userEvent.click(...)`** — always await interactions that trigger async handlers. |
| 194 | + |
| 195 | +### Examples |
| 196 | + |
| 197 | +```tsx |
| 198 | +// ❌ BAD — immediate assertion after render; component fetches data on mount |
| 199 | +render(<ParticipantReports />); |
| 200 | +expect(screen.getByLabelText(/enter animal ids/i)).toBeInTheDocument(); |
| 201 | + |
| 202 | +// ✅ GOOD — waits for async mount to settle before asserting |
| 203 | +render(<ParticipantReports />); |
| 204 | +await waitFor(() => { |
| 205 | + expect(screen.getByText('General')).toBeVisible(); |
| 206 | +}); |
| 207 | +expect(screen.getByLabelText(/enter animal ids/i)).toBeInTheDocument(); |
| 208 | +``` |
0 commit comments