Skip to content

Commit 7605253

Browse files
AI front end code review guidelines and claude skills (#1292)
- .agents/review-checklists/jest code review checks - .agents/review-checklists/react code review checks - .agents/review-checkslists/common.md general code review guidelines - `.claude/skill/<skill name>/skill.md` define the claude skill using these guidelines --------- Co-authored-by: Alan Vezina <alanv@labkey.com>
1 parent e75eb24 commit 7605253

9 files changed

Lines changed: 1483 additions & 0 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Common Review Guidelines
2+
3+
## Reviewer Priority
4+
5+
Agents must prioritize findings in this order and report them in this order when multiple issues are present:
6+
7+
1. **Correctness** (behavioral bugs, warnings, broken contracts, stale state, invalid keys)
8+
2. **Maintainability** (dead code, duplication, unnecessary complexity, high-review-cost risks)
9+
3. **Style** (formatting/convention consistency that does not change behavior)
10+
11+
Do not prioritize Style-only findings ahead of unresolved Correctness or Maintainability findings.
12+
13+
## Standard Review Format
14+
15+
For every rule below, agents should provide:
16+
17+
1. **Evidence**: exact code snippet or file/line reference from the changed code
18+
2. **Category**: the rule's listed primary category (Correctness, Maintainability, or Style)
19+
3. **Confidence**: apply the rule's confidence threshold before escalating severity in review feedback
20+
4. **Exceptions**: check the rule's false-positive section before flagging
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Business Logic
2+
3+
> **Prerequisite:** Review and apply the common guidelines in [`common.md`](../common.md) before using this checklist.
4+
5+
## Jest tests must assert on meaningful outcomes
6+
7+
**Urgency:** urgent
8+
9+
### Category
10+
11+
Correctness
12+
13+
### Confidence Threshold
14+
15+
Flag as a high-severity finding when the test would still pass after removing the feature logic, or when assertions only validate setup/static existence. If the test is intentionally a smoke/no-crash render test, require that intent to be explicit in the test name.
16+
17+
### Exceptions / False Positives
18+
19+
- Allow explicit smoke tests when the purpose is only to verify the component/function does not throw on render or initialization.
20+
- A simple existence assertion can be acceptable when the behavior under review is conditional presence/absence itself (for example, permission-gated rendering).
21+
22+
### Detection heuristic
23+
24+
Look for tests where all assertions target mock setup data, static strings, or DOM existence rather than computed/rendered output.
25+
26+
## Rules
27+
28+
1. **Assert on component output, not existence.** After `render()`, assert on visible text, element states, or DOM changes that result from the props/state you set up — never just `expect(container).toBeDefined()` or `expect(document.querySelector('.x')).not.toBeNull()`.
29+
30+
2. **Assert on computed results, not test inputs.** If you pass `mockData` into a component, don't assert that `mockData` has the values you just wrote. Assert on what the component *did* with that data.
31+
32+
3. **Every test must fail if the feature is removed.** Apply this litmus test: if you deleted the implementation code for the feature under test, would the test still pass? If yes, the test is worthless — rewrite it.
33+
34+
4. **Interact before asserting (when applicable).** If testing behavior triggered by user action (click, submit, input), simulate that action with `fireEvent` or `userEvent`, then assert on the resulting DOM or state change.
35+
36+
## Examples
37+
38+
```js
39+
// ❌ BAD — asserts on render existence
40+
render(<MyForm valid={false} />);
41+
expect(document.querySelector('.form-container')).not.toBeNull();
42+
43+
// ✅ GOOD — asserts on behavioral outcome of props
44+
render(<MyForm valid={false} />);
45+
expect(screen.getByRole('button', { name: /submit/i })).toBeDisabled();
46+
47+
// ❌ BAD — asserts on mock input
48+
const data = [{ name: 'Alpha', value: 10 }];
49+
render(<Table rows={data} />);
50+
expect(data[0].name).toBe('Alpha'); // This tests your test, not your code
51+
52+
// ✅ GOOD — asserts on rendered output from that input
53+
const data = [{ name: 'Alpha', value: 10 }];
54+
render(<Table rows={data} />);
55+
const row = screen.getByRole('row', { name: /alpha\s+10/i });
56+
expect(within(row).getByRole('cell', { name: 'Alpha' })).toBeInTheDocument();
57+
expect(within(row).getByRole('cell', { name: '10' })).toBeInTheDocument();
58+
```
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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+
```
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Performance
2+
3+
> **Prerequisite:** Review and apply the common guidelines in [`common.md`](../common.md) before using this checklist.
4+
5+
## No redundant or near-duplicate tests
6+
7+
**Urgency:** suggestion
8+
9+
### Category
10+
11+
Maintainability
12+
13+
### Confidence Threshold
14+
15+
Flag when two tests clearly exercise the same code path with identical setup/action and only trivial literal differences. If separate tests improve failure localization or document distinct branches, prefer a suggestion over a required merge.
16+
17+
### Exceptions / False Positives
18+
19+
- Do not merge tests that exercise different branches, error paths, permission states, or feature flags even if they look similar.
20+
- Keep separate tests when splitting assertions materially improves failure diagnostics or readability for a complex behavior.
21+
- Do not force `test.each` if parameterization would obscure the intent or make debugging harder than explicit tests.
22+
23+
### Rules
24+
25+
1. **Merge tests that assert the same behavior with different literal values.** If two or more tests render the same component, trigger the same interaction, and assert the same outcome shape — differing only in the data passed in — combine them into a single `test.each` or a single test with multiple representative cases.
26+
2. **Merge tests whose Arrange and Act steps are identical.** If two tests set up the same state and perform the same action but assert on different aspects of the result, combine them into one test with multiple assertions. A single test with several `expect` calls is preferable to duplicate setup/teardown overhead.
27+
3. **Remove tests that are strict subsets of another test.** If test A asserts that a button is rendered and test B asserts that clicking the same button fires a callback, test A is redundant — the click in test B already proves the button exists. **Exception:** keep the existence test if it tests a different conditional path than the behavioral test (e.g., the existence test renders with restricted permissions while the click test renders with full permissions).
28+
4. **Keep separate tests for genuinely distinct code paths.** Two tests that look similar but exercise different branches (e.g., an error path vs. a success path, an empty list vs. a populated list) are not duplicates and must remain separate.
29+
30+
### Examples
31+
32+
```tsx
33+
// ❌ BAD — three nearly identical tests differing only in input label
34+
test('renders label for first name', () => {
35+
render(<FormField label="First Name" />);
36+
expect(screen.getByText('First Name')).toBeInTheDocument();
37+
});
38+
39+
test('renders label for last name', () => {
40+
render(<FormField label="Last Name" />);
41+
expect(screen.getByText('Last Name')).toBeInTheDocument();
42+
});
43+
44+
test('renders label for email', () => {
45+
render(<FormField label="Email" />);
46+
expect(screen.getByText('Email')).toBeInTheDocument();
47+
});
48+
49+
// ✅ GOOD — combined into a parameterized test
50+
test.each(['First Name', 'Last Name', 'Email'])('renders label "%s"', (label) => {
51+
render(<FormField label={label} />);
52+
expect(screen.getByText(label)).toBeInTheDocument();
53+
});
54+
55+
56+
// ❌ BAD — two tests with identical Arrange/Act, different Assert
57+
test('disables submit when form is invalid', () => {
58+
render(<MyForm valid={false} />);
59+
expect(screen.getByRole('button', { name: /submit/i })).toBeDisabled();
60+
});
61+
62+
test('shows validation message when form is invalid', () => {
63+
render(<MyForm valid={false} />);
64+
expect(screen.getByText('Please fix errors above')).toBeInTheDocument();
65+
});
66+
67+
// ✅ GOOD — combined into one test with both assertions
68+
test('shows validation state when form is invalid', () => {
69+
render(<MyForm valid={false} />);
70+
expect(screen.getByRole('button', { name: /submit/i })).toBeDisabled();
71+
expect(screen.getByText('Please fix errors above')).toBeInTheDocument();
72+
});
73+
74+
75+
// ❌ BAD — subset test is redundant
76+
test('renders the delete button', () => {
77+
render(<ItemRow item={mockItem} onDelete={mockDelete} />);
78+
expect(screen.getByRole('button', { name: /delete/i })).toBeInTheDocument();
79+
});
80+
81+
test('calls onDelete when delete button is clicked', () => {
82+
render(<ItemRow item={mockItem} onDelete={mockDelete} />);
83+
fireEvent.click(screen.getByRole('button', { name: /delete/i }));
84+
expect(mockDelete).toHaveBeenCalledWith(mockItem.id);
85+
});
86+
87+
// ✅ GOOD — only the behavioral test remains (it implicitly proves the button exists)
88+
test('calls onDelete when delete button is clicked', () => {
89+
render(<ItemRow item={mockItem} onDelete={mockDelete} />);
90+
fireEvent.click(screen.getByRole('button', { name: /delete/i }));
91+
expect(mockDelete).toHaveBeenCalledWith(mockItem.id);
92+
});
93+
94+
95+
// ✅ OK — these look similar but test genuinely different code paths
96+
test('renders empty state when no items', () => {
97+
render(<ItemList items={[]} />);
98+
expect(screen.getByText('No items found')).toBeInTheDocument();
99+
});
100+
101+
test('renders item rows when items are provided', () => {
102+
render(<ItemList items={[mockItem]} />);
103+
expect(screen.getByText(mockItem.name)).toBeInTheDocument();
104+
});
105+
```

0 commit comments

Comments
 (0)