-
Notifications
You must be signed in to change notification settings - Fork 11
AI front end code review guidelines and claude skills #1292
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| # Common Review Guidelines | ||
|
|
||
| ## Reviewer Priority | ||
|
|
||
| Agents must prioritize findings in this order and report them in this order when multiple issues are present: | ||
|
|
||
| 1. **Correctness** (behavioral bugs, warnings, broken contracts, stale state, invalid keys) | ||
| 2. **Maintainability** (dead code, duplication, unnecessary complexity, high-review-cost risks) | ||
| 3. **Style** (formatting/convention consistency that does not change behavior) | ||
|
|
||
| Do not prioritize Style-only findings ahead of unresolved Correctness or Maintainability findings. | ||
|
|
||
| ## Standard Review Format | ||
|
|
||
| For every rule below, agents should provide: | ||
|
|
||
| 1. **Evidence**: exact code snippet or file/line reference from the changed code | ||
| 2. **Category**: the rule's listed primary category (Correctness, Maintainability, or Style) | ||
| 3. **Confidence**: apply the rule's confidence threshold before escalating severity in review feedback | ||
| 4. **Exceptions**: check the rule's false-positive section before flagging |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| # Business Logic | ||
|
|
||
| > **Prerequisite:** Review and apply the common guidelines in [`common.md`](../common.md) before using this checklist. | ||
|
|
||
| ## Jest tests must assert on meaningful outcomes | ||
|
|
||
| **Urgency:** urgent | ||
|
|
||
| ### Category | ||
|
|
||
| Correctness | ||
|
|
||
| ### Confidence Threshold | ||
|
|
||
| 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. | ||
|
|
||
| ### Exceptions / False Positives | ||
|
|
||
| - Allow explicit smoke tests when the purpose is only to verify the component/function does not throw on render or initialization. | ||
| - Do not flag helper tests that intentionally validate test utilities/builders rather than product behavior, if the subject under test is the utility itself. | ||
| - A simple existence assertion can be acceptable when the behavior under review is conditional presence/absence itself (for example, permission-gated rendering). | ||
|
|
||
| ### Detection heuristic | ||
|
|
||
| Look for tests where all assertions target mock setup data, static strings, or DOM existence rather than computed/rendered output. | ||
|
|
||
| ## Rules | ||
|
|
||
| 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()`. | ||
|
|
||
| 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. | ||
|
|
||
| 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. | ||
|
|
||
| 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. | ||
|
|
||
| ## Examples | ||
|
|
||
| ```js | ||
| // ❌ BAD — asserts on render existence | ||
| render(<MyForm valid={false} />); | ||
| expect(document.querySelector('.form-container')).not.toBeNull(); | ||
|
|
||
| // ✅ GOOD — asserts on behavioral outcome of props | ||
| render(<MyForm valid={false} />); | ||
| expect(screen.getByRole('button', { name: /submit/i })).toBeDisabled(); | ||
|
|
||
| // ❌ BAD — asserts on mock input | ||
| const data = [{ name: 'Alpha', value: 10 }]; | ||
| render(<Table rows={data} />); | ||
| expect(data[0].name).toBe('Alpha'); // This tests your test, not your code | ||
|
|
||
| // ✅ GOOD — asserts on rendered output from that input | ||
| const data = [{ name: 'Alpha', value: 10 }]; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we should encourage even more specific checks here. I think keeping this section is good, but maybe adding something that says "instead of looking for the existence of data anywhere, look for the existence of data where it is expected to be". Then provide an example of looking for "name" to be in the first column of the example Table component here, or something similar. Just generically looking for content to be on the page may be so overbroad that it is not useful. |
||
| render(<Table rows={data} />); | ||
| expect(screen.getByText('Alpha')).toBeInTheDocument(); | ||
| expect(screen.getByText('10')).toBeInTheDocument(); | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,208 @@ | ||
| # Code Quality | ||
|
|
||
| > **Prerequisite:** Review and apply the common guidelines in [`common.md`](../common.md) before using this checklist. | ||
|
|
||
| ## Arrange / Act / Assert (AAA) structure | ||
|
|
||
| **Urgency:** suggestion | ||
|
|
||
| ### Category | ||
|
|
||
| Style | ||
|
|
||
| ### Confidence Threshold | ||
|
|
||
| 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. | ||
|
|
||
| ### Exceptions / False Positives | ||
|
|
||
| - Do not flag short tests (roughly 3-5 lines) where Arrange/Act/Assert is obvious without comments. | ||
| - Do not require AAA comments in parameterized tests when added comments make the test harder to scan than the code itself. | ||
| - Prefer suggestions over high-severity findings unless the repository explicitly enforces AAA comment structure in tests. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure this is useful, since we don't have any repositories where AAA comments are explicitly enforced, and most of our existing tests do not follow this pattern. |
||
|
|
||
| ### Rules | ||
|
|
||
| 1. **Each section must be preceded by a comment** — `// Arrange`, `// Act`, and `// Assert`. | ||
| 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. | ||
| - Good: `// Assert - textarea shows hash subjects, participantId query param is ignored` | ||
| - Good: `// Assert - ID Search button is active` | ||
| - Bad: `// Assert` (missing explanation) | ||
| - Bad: `// Assert - check results` (too vague; does not connect to the scenario) | ||
| 3. **Arrange may be omitted** when there is no setup beyond what `beforeEach` already provides, but Act and Assert are always required. | ||
| 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: | ||
| - Good: `// Act & Assert - empty state message is shown and error was logged to console` | ||
| - Bad: `// Act & Assert` | ||
| 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. | ||
|
|
||
| --- | ||
|
|
||
| ## No unused variables, imports, or mocks | ||
|
|
||
| **Urgency:** urgent | ||
|
|
||
| ### Category | ||
|
|
||
| Maintainability | ||
|
|
||
| ### Confidence Threshold | ||
|
|
||
| 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. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what does "the symbol" mean in this sentence? do you mean variable? |
||
|
|
||
| ### Exceptions / False Positives | ||
|
|
||
| - Do not flag side-effect imports (for example, polyfills or test environment setup) just because no identifier is referenced. | ||
| - Do not flag module-level `jest.mock(...)` declarations that intentionally replace imports used elsewhere in the file. | ||
| - If a placeholder parameter is required for function signature/position, allow underscore-prefixed names (for example, `_arg`). | ||
|
|
||
| ### Rules | ||
|
|
||
| 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. | ||
| 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. | ||
| 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. | ||
| 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. | ||
| 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. | ||
| 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`). | ||
| 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. | ||
|
|
||
| ### Examples | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is great. I'd love to not have to manually check as a code reviewer if the import or variable is used! |
||
|
|
||
| ```tsx | ||
| // ---- Unused imports ---- | ||
|
|
||
| // ❌ BAD — ApiResponse is imported but never used | ||
| import { render, screen } from '@testing-library/react'; | ||
| import { ApiResponse, UserProfile } from '../models'; | ||
|
|
||
| const mockProfile: UserProfile = { name: 'Test' }; | ||
|
|
||
| // ✅ GOOD — only referenced imports remain | ||
| import { render, screen } from '@testing-library/react'; | ||
| import { UserProfile } from '../models'; | ||
|
|
||
| const mockProfile: UserProfile = { name: 'Test' }; | ||
|
|
||
|
|
||
| // ---- Unused variables ---- | ||
|
|
||
| // ❌ BAD — mockHandler is declared but never used | ||
| const mockHandler = jest.fn(); | ||
| const mockCallback = jest.fn(); | ||
|
|
||
| test('calls callback on click', () => { | ||
| render(<Button onClick={mockCallback} />); | ||
| fireEvent.click(screen.getByRole('button')); | ||
| expect(mockCallback).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| // ✅ GOOD — only mockCallback remains | ||
| const mockCallback = jest.fn(); | ||
|
|
||
| test('calls callback on click', () => { | ||
| render(<Button onClick={mockCallback} />); | ||
| fireEvent.click(screen.getByRole('button')); | ||
| expect(mockCallback).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
|
|
||
| // ---- Unused mock setup ---- | ||
|
|
||
| // ❌ BAD — jest.spyOn for console.warn is set up but never asserted or needed | ||
| jest.spyOn(console, 'warn').mockImplementation(() => {}); | ||
| jest.spyOn(console, 'error').mockImplementation(() => {}); | ||
|
|
||
| test('renders error state', () => { | ||
| render(<ErrorBanner message="fail" />); | ||
| expect(screen.getByText('fail')).toBeInTheDocument(); | ||
| expect(console.error).toHaveBeenCalled(); | ||
| // console.warn is never checked | ||
| }); | ||
|
|
||
| // ✅ GOOD — only the console.error spy remains | ||
| jest.spyOn(console, 'error').mockImplementation(() => {}); | ||
|
|
||
| test('renders error state', () => { | ||
| render(<ErrorBanner message="fail" />); | ||
| expect(screen.getByText('fail')).toBeInTheDocument(); | ||
| expect(console.error).toHaveBeenCalled(); | ||
| }); | ||
|
|
||
|
|
||
| // ---- Unused render destructuring ---- | ||
|
|
||
| // ❌ BAD — container is destructured but never referenced | ||
| const { container } = render(<Header title="Hello" />); | ||
| expect(screen.getByText('Hello')).toBeInTheDocument(); | ||
|
|
||
| // ✅ GOOD — render called without unused destructuring | ||
| render(<Header title="Hello" />); | ||
| expect(screen.getByText('Hello')).toBeInTheDocument(); | ||
|
|
||
|
|
||
| // ---- Unused callback parameter ---- | ||
|
|
||
| // ❌ BAD — `req` is not used but has no underscore prefix | ||
| mockFetch.mockImplementation((req, options) => { | ||
| return Promise.resolve({ ok: true }); | ||
| }); | ||
|
|
||
| // ✅ GOOD — unused positional parameter prefixed with _ | ||
| mockFetch.mockImplementation((_req, options) => { | ||
| return Promise.resolve({ ok: true }); | ||
| }); | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Async React updates must be awaited (`act` warning prevention) | ||
|
|
||
| **Urgency:** urgent | ||
|
|
||
| ### Category | ||
|
|
||
| Correctness | ||
|
|
||
| ### Confidence Threshold | ||
|
|
||
| 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. | ||
|
|
||
| ### Exceptions / False Positives | ||
|
|
||
| - Do not require `await` for purely synchronous interactions that do not trigger async state updates. | ||
| - If the test uses a project helper that already waits for async UI stabilization, avoid duplicating `waitFor`/`findBy` calls. | ||
| - When fake timers are used, accept explicit `act(...)` + timer advancement patterns that correctly flush updates. | ||
|
|
||
| ### Rules | ||
|
|
||
| 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. | ||
| 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. | ||
| 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. | ||
| 4. **Presence of `act(...)` warnings in test output is a defect, even when tests pass.** Treat these warnings as test failures during review. | ||
|
|
||
| ### Heuristics for detection | ||
|
|
||
| - `render(...)` followed by immediate assertions while the component has `useEffect(() => { fetch(...).then(...) }, [])` or similar async-on-mount logic. | ||
| - `userEvent.click/type/...` followed by immediate assertions that depend on async updates. | ||
| - Presence of `console.error` `act(...)` warnings in test output, even when tests pass. | ||
| - Test functions not marked `async` despite the component performing async work. | ||
|
|
||
| ### Preferred fixes (in order of preference) | ||
|
|
||
| 1. **`await screen.findBy...(...)` — preferred.** Use for elements that appear after async work. Simpler and more idiomatic than `waitFor`. | ||
| 2. **`await waitFor(() => expect(...))`** — use when asserting on state-dependent conditions that `findBy` can't express (e.g., element attribute changes, disappearance). | ||
| 3. **Shared helpers** like `waitForComponentToLoad()` — call after render when available. | ||
| 4. **`await userEvent.click(...)`** — always await interactions that trigger async handlers. | ||
|
|
||
| ### Examples | ||
|
|
||
| ```tsx | ||
| // ❌ BAD — immediate assertion after render; component fetches data on mount | ||
| render(<ParticipantReports />); | ||
| expect(screen.getByLabelText(/enter animal ids/i)).toBeInTheDocument(); | ||
|
|
||
| // ✅ GOOD — waits for async mount to settle before asserting | ||
| render(<ParticipantReports />); | ||
| await waitFor(() => { | ||
| expect(screen.getByText('General')).toBeVisible(); | ||
| }); | ||
| expect(screen.getByLabelText(/enter animal ids/i)).toBeInTheDocument(); | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| # Performance | ||
|
|
||
| > **Prerequisite:** Review and apply the common guidelines in [`common.md`](../common.md) before using this checklist. | ||
|
|
||
| ## No redundant or near-duplicate tests | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm curious what AI would consider a near-duplicate test. I think using near duplicate tests is exactly how test cases often look when you are testing interactions of a sequence of parameters or properties (i.e. adjust prop1, then prop2, then prop3, etc. but the test is nearly the same).
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, in those cases we should be using something like |
||
|
|
||
| **Urgency:** suggestion | ||
|
|
||
| ### Category | ||
|
|
||
| Maintainability | ||
|
|
||
| ### Confidence Threshold | ||
|
|
||
| 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. | ||
|
|
||
| ### Exceptions / False Positives | ||
|
|
||
| - Do not merge tests that exercise different branches, error paths, permission states, or feature flags even if they look similar. | ||
| - Keep separate tests when splitting assertions materially improves failure diagnostics or readability for a complex behavior. | ||
| - Do not force `test.each` if parameterization would obscure the intent or make debugging harder than explicit tests. | ||
|
|
||
| ### Rules | ||
|
|
||
| 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. | ||
| 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. | ||
| 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). | ||
| 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. | ||
|
|
||
| ### Examples | ||
|
|
||
| ```tsx | ||
| // ❌ BAD — three nearly identical tests differing only in input label | ||
| test('renders label for first name', () => { | ||
| render(<FormField label="First Name" />); | ||
| expect(screen.getByText('First Name')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| test('renders label for last name', () => { | ||
| render(<FormField label="Last Name" />); | ||
| expect(screen.getByText('Last Name')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| test('renders label for email', () => { | ||
| render(<FormField label="Email" />); | ||
| expect(screen.getByText('Email')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| // ✅ GOOD — combined into a parameterized test | ||
| test.each(['First Name', 'Last Name', 'Email'])('renders label "%s"', (label) => { | ||
| render(<FormField label={label} />); | ||
| expect(screen.getByText(label)).toBeInTheDocument(); | ||
| }); | ||
|
|
||
|
|
||
| // ❌ BAD — two tests with identical Arrange/Act, different Assert | ||
| test('disables submit when form is invalid', () => { | ||
| render(<MyForm valid={false} />); | ||
| expect(screen.getByRole('button', { name: /submit/i })).toBeDisabled(); | ||
| }); | ||
|
|
||
| test('shows validation message when form is invalid', () => { | ||
| render(<MyForm valid={false} />); | ||
| expect(screen.getByText('Please fix errors above')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| // ✅ GOOD — combined into one test with both assertions | ||
| test('shows validation state when form is invalid', () => { | ||
| render(<MyForm valid={false} />); | ||
| expect(screen.getByRole('button', { name: /submit/i })).toBeDisabled(); | ||
| expect(screen.getByText('Please fix errors above')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
|
|
||
| // ❌ BAD — subset test is redundant | ||
| test('renders the delete button', () => { | ||
| render(<ItemRow item={mockItem} onDelete={mockDelete} />); | ||
| expect(screen.getByRole('button', { name: /delete/i })).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| test('calls onDelete when delete button is clicked', () => { | ||
| render(<ItemRow item={mockItem} onDelete={mockDelete} />); | ||
| fireEvent.click(screen.getByRole('button', { name: /delete/i })); | ||
| expect(mockDelete).toHaveBeenCalledWith(mockItem.id); | ||
| }); | ||
|
|
||
| // ✅ GOOD — only the behavioral test remains (it implicitly proves the button exists) | ||
| test('calls onDelete when delete button is clicked', () => { | ||
| render(<ItemRow item={mockItem} onDelete={mockDelete} />); | ||
| fireEvent.click(screen.getByRole('button', { name: /delete/i })); | ||
| expect(mockDelete).toHaveBeenCalledWith(mockItem.id); | ||
| }); | ||
|
|
||
|
|
||
| // ✅ OK — these look similar but test genuinely different code paths | ||
| test('renders empty state when no items', () => { | ||
| render(<ItemList items={[]} />); | ||
| expect(screen.getByText('No items found')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| test('renders item rows when items are provided', () => { | ||
| render(<ItemList items={[mockItem]} />); | ||
| expect(screen.getByText(mockItem.name)).toBeInTheDocument(); | ||
| }); | ||
| ``` | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure I understand this rule. Are we ever testing our test utilities?