.agents/skills/make-tests/SKILL.md
# Make Tests Skill **Target:** `<PR_NUMBER | BRANCH_NAME>` > **Important:** If no argument was provided above (empty or missing), use the AskUserQuestion tool to ask the user what they want to create tests for. They can provide: > > - A PR number (format: `#123` or `123`) > - A branch name (local or remote, e.g., `feature/my-feature` or `origin/feature/my-feature`) ## Input Detection Determine the input type in this order: ### Step 1: Check if PR Number (Remote) If the argument is numeric
npx skillsauth add getlago/lago-front .agents/skills/make-testsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Target: <PR_NUMBER | BRANCH_NAME>
Important: If no argument was provided above (empty or missing), use the AskUserQuestion tool to ask the user what they want to create tests for. They can provide:
- A PR number (format:
#123or123)- A branch name (local or remote, e.g.,
feature/my-featureororigin/feature/my-feature)
Determine the input type in this order:
If the argument is numeric or starts with # followed by numbers (e.g., #123, 123):
# prefix if presentgh pr view <PR_NUMBER> --json numberIf the argument is not a PR number:
git branch --list <BRANCH_NAME>If the branch doesn't exist locally:
git fetch origingit branch -r --list origin/<BRANCH_NAME>If no valid input is detected or the branch doesn't exist:
git branch --show-currentThis skill creates comprehensive tests for code changes in a GitHub Pull Request or a git branch, following the established patterns and conventions in this codebase.
This skill will:
main)Test by default, justify skips. Every new or significantly modified file SHOULD get tests unless it falls into the Explicit Exclusions list below. The bias is toward testing — if in doubt, write the test.
Every new file gets tests unless it matches an Explicit Exclusion. This includes:
A file has testable logic if it contains ANY of the following. Even ONE match = MUST test:
useState, useEffect, useLayoutEffect, useMemo, useCallbackuseNavigate, useParams, useSearchParams or any routing hookuse*)useQuery, useLazyQuery, useMutation){condition && ...}, ternary in JSX)onClick, onChange, onSubmit)if/else, switch, ternary operators in logic.map(), .filter(), .reduce() on dataA file is "pure presentational" ONLY if it literally does nothing but return static JSX with props interpolation — no hooks, no conditions, no handlers, no state. This is extremely rare in practice.
You may skip tests ONLY for files that are:
.d.ts, type-only files)generated/graphql.tsx, codegen output)translations/*.json)If a file does not match the above list, it MUST be tested. When in doubt, test it.
If you decide not to test a file, you MUST document WHY in the coverage map (Phase 2). Valid reasons:
Invalid reasons (do NOT use these to skip tests):
Before starting, gather context by reading these reference files:
@.agents/docs/testing-practices.md@.agents/docs/code-quality.mdsrc/components/invoices/details/__tests__/InvoiceDetailsTable.integration.test.tsxsrc/test-utils.tsxExecute the following logic to detect input type and fetch the diff:
# Store the input argument
INPUT="$ARGUMENTS"
# Step 1: Check if PR Number
if [[ "$INPUT" =~ ^#?[0-9]+$ ]]; then
PR_NUMBER="${INPUT#\#}" # Remove # prefix if present
# Verify PR exists
if gh pr view "$PR_NUMBER" --json number &>/dev/null; then
echo "Detected: PR #$PR_NUMBER"
# Fetch PR info and diff
gh pr view "$PR_NUMBER" --json files,additions,deletions,body,title
gh pr diff "$PR_NUMBER"
exit 0
fi
fi
# Step 2: Check if Local Branch
if git branch --list "$INPUT" | grep -q "$INPUT"; then
echo "Detected: Local branch '$INPUT'"
git diff main..."$INPUT" --name-only
git diff main..."$INPUT"
exit 0
fi
# Step 3: Check if Remote Branch
git fetch origin &>/dev/null
REMOTE_BRANCH="${INPUT#origin/}" # Remove origin/ prefix if present
if git branch -r --list "origin/$REMOTE_BRANCH" | grep -q "origin/$REMOTE_BRANCH"; then
echo "Detected: Remote branch 'origin/$REMOTE_BRANCH'"
git diff main...origin/"$REMOTE_BRANCH" --name-only
git diff main...origin/"$REMOTE_BRANCH"
exit 0
fi
# Step 4: Fallback to current branch
CURRENT_BRANCH=$(git branch --show-current)
echo "Input '$INPUT' not found. Using current branch: '$CURRENT_BRANCH'"
git diff main...HEAD --name-only
git diff main...HEAD
| Input Type | Example | Diff Command |
| -------------- | -------------------- | ------------------------------------ |
| PR Number | #123, 123 | gh pr diff 123 |
| Local Branch | feature/my-feature | git diff main...feature/my-feature |
| Remote Branch | origin/feature/x | git diff main...origin/feature/x |
| Current Branch | (fallback) | git diff main...HEAD |
Analyze the changed files and categorize them. Default bias: TEST.
Files that ALWAYS get tests (no exceptions):
.tsx files in src/components/, src/pages/).ts files in src/hooks/).ts files in src/core/utils/, src/utils/)Files that may be skipped (Explicit Exclusions only):
.d.ts, type-only files)generated/graphql.tsx, codegen output)translations/*.json).css, .scss)Every file that is NOT in the exclusion list above MUST be tested. When in doubt, test it.
For each file requiring tests, briefly assess the scope:
This assessment determines HOW MANY tests to write, not WHETHER to test.
Before creating tests, verify that ALL new files in the PR are accounted for:
1. Supporting Files (schemas, configs, types)
2. Custom Hooks
3. New Patterns Introduced by the PR
CRITICAL: Before proceeding to test planning and before asking for user approval, you MUST generate a complete table listing EVERY SINGLE FILE changed in the PR or branch. No files may be omitted or grouped. If the PR has 50 files changed, the table MUST have exactly 50 rows.
For each file in the PR/branch diff, collect:
NEW (added), Modified (changed), DELETED (removed), Renamed✅ YES or ⏭️ NO)⏭️ NO, provide a short reason (e.g., "Type definitions", "Generated file", "Pure presentational", "Config file", "Translation file", "Barrel export", "Simple prop pass-through", "Tested implicitly via ComponentName", "Deleted file", "Snapshot update")Use the following exact format. Every file MUST appear as its own row:
Complete File List — PR #<PR_NUMBER> (or Branch: <BRANCH_NAME>)
┌─────┬────────────────────────────────────────────────────────────────┬───────────┬──────────┬───────┬─────────────────────────────────┐
│ # │ File │ +/- │ Status │ Test? │ Skip Reason │
├─────┼────────────────────────────────────────────────────────────────┼───────────┼──────────┼───────┼─────────────────────────────────┤
│ 1 │ src/components/example/Component.tsx │ +122/-0 │ NEW │ ✅ YES │ │
├─────┼────────────────────────────────────────────────────────────────┼───────────┼──────────┼───────┼─────────────────────────────────┤
│ 2 │ src/components/example/types.ts │ +15/-0 │ NEW │ ⏭️ NO │ Type definitions │
├─────┼────────────────────────────────────────────────────────────────┼───────────┼──────────┼───────┼─────────────────────────────────┤
│ 3 │ src/generated/graphql.tsx │ +267/-190 │ Modified │ ⏭️ NO │ Generated file │
├─────┼────────────────────────────────────────────────────────────────┼───────────┼──────────┼───────┼─────────────────────────────────┤
│ 4 │ translations/base.json │ +17/-4 │ Modified │ ⏭️ NO │ Translation file │
└─────┴────────────────────────────────────────────────────────────────┴───────────┴──────────┴───────┴─────────────────────────────────┘
# column is a sequential counter starting from 1+/- column shows additions and deletions (e.g., +122/-0, +8/-12, +0/-64)Status column uses: NEW for added files, Modified for changed files, DELETED for removed files, Renamed for renamed filesTest? column uses: ✅ YES for files that will have tests, ⏭️ NO for files that won'tSkip Reason column is empty for files marked ✅ YES, and provides a concise reason for files marked ⏭️ NOTotal: X files | Will test: Y files | Skipped: Z filesAfter displaying the table, ask the user to confirm:
Do NOT proceed to Phase 2 until the user approves the table.
Before writing any test, you MUST create a coverage map that accounts for EVERY new or significantly changed file in the PR.
The coverage map must list ALL added or modified files (NOT deleted files) with their test status.
Rules for classifying files — apply the "Testable Logic Checklist" from the Philosophy section:
useState, useEffect, useMemo, useCallback, useNavigate, useParams, useSearchParams, custom hooks (use*), GraphQL queries/mutations, conditional rendering, event handlers, .map()/.filter()/.reduce(), toast/clipboard/dialog operationsFor MODIFIED files (not just new files):
| # | File | Type | Lines Added | Test File | Status | Skip Reason |
| --- | ----------------------------------- | --------- | ----------- | ---------------------------------------- | ------------ | --------------------- |
| 1 | `src/pages/WebhookForm.tsx` | Page | 327 | `__tests__/WebhookForm.test.tsx` | ✅ WILL TEST | — |
| 2 | `src/hooks/useWebhookEventTypes.ts` | Hook | 211 | `__tests__/useWebhookEventTypes.test.ts` | ✅ WILL TEST | — |
| 3 | `src/hooks/useDeleteWebhook.ts` | Hook | 50 | `__tests__/useDeleteWebhook.test.ts` | ✅ WILL TEST | — |
| 4 | `src/types/webhook.ts` | Types | 15 | — | ⏭️ SKIP | Pure type definitions |
| 5 | `src/generated/graphql.tsx` | Generated | 267 | — | ⏭️ SKIP | Generated file |
Rules for the coverage map:
⛔ STOP: Before writing any test, you MUST present the coverage map to the user and ask for confirmation.
Present the table from Step 2.1 to the user and ask:
"Here is the coverage map for this PR. I will create test files for all ✅ items and skip ⏭️ items for the reasons listed. Do you want me to proceed, or would you like to adjust any decisions?"
Do NOT proceed to Phase 3 until the user confirms. The user may:
For each file marked ✅ WILL TEST, briefly identify key test scenarios:
### File: `src/path/to/Component.tsx`
**Key scenarios to test:**
- Default rendering with required props
- Conditional rendering paths (lines X-Y)
- User interactions (form submit, button clicks)
- Error/loading states
- Edge cases (empty data, invalid input)
Keep this brief — the goal is to plan, not to over-analyze. If a file has logic, test it.
Before creating new mocks, search for existing ones:
# Search for existing factories
find src -name "*factory*" -o -name "*Factory*" | head -20
# Search for existing mocks
find src -name "*mock*" -o -name "*Mock*" | head -20
# Search for shared test utilities
ls -la src/__mocks__/ 2>/dev/null || echo "No shared mocks folder"
CRITICAL: Before writing tests, add data-test constants to the component being tested.
In the component file:
// Export data-test constants at the top of the component file (after imports)
export const COMPONENT_NAME_TEST_ID = 'component-name'
export const COMPONENT_NAME_TITLE_TEST_ID = 'component-name-title'
export const COMPONENT_NAME_SUBMIT_BUTTON_TEST_ID = 'component-name-submit-button'
export const COMPONENT_NAME_ERROR_MESSAGE_TEST_ID = 'component-name-error-message'
// Add more as needed for testable elements
export const ComponentName = ({ ... }) => {
return (
<div data-test={COMPONENT_NAME_TEST_ID}>
<Typography data-test={COMPONENT_NAME_TITLE_TEST_ID}>
{translate('...')}
</Typography>
{/* For form.SubmitButton use dataTest (camelCase) */}
<form.SubmitButton dataTest={COMPONENT_NAME_SUBMIT_BUTTON_TEST_ID}>
Submit
</form.SubmitButton>
</div>
)
}
Naming convention:
{COMPONENT_NAME}_{ELEMENT_DESCRIPTION}_TEST_IDNEVER wrap elements in extra <div> just to add a data-test attribute:
Adding a wrapper <div> (even without styles) can break the UI due to cascading CSS, flexbox/grid layout inheritance, or fragment-based rendering assumptions.
// ❌ WRONG - Do NOT wrap fragments or elements in a <div> just for data-test
// Original code:
<>
<Skeleton variant="text" className="w-60" textVariant="headline" />
<Skeleton variant="text" className="w-40" textVariant="body" />
</>
// ❌ NEVER do this:
<div data-test={COMPONENT_LOADING_TEST_ID}>
<Skeleton variant="text" className="w-60" textVariant="headline" />
<Skeleton variant="text" className="w-40" textVariant="body" />
</div>
Rule: Only add data-test to elements that already exist in the JSX. If an element cannot accept data-test natively (e.g., React fragments <>, third-party components without data-test prop support), do not test that element rather than wrapping it in a <div>. Skipping a test is always preferable to altering the component's DOM structure.
Create test file at: src/path/to/__tests__/ComponentName.test.tsx
Test file structure:
import { screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import {
COMPONENT_NAME_TEST_ID,
COMPONENT_NAME_TITLE_TEST_ID,
COMPONENT_NAME_SUBMIT_BUTTON_TEST_ID,
ComponentName,
} from '../ComponentName'
import { render } from '~/test-utils'
// Mock dependencies
jest.mock('~/hooks/core/useInternationalization', () => ({
useInternationalization: () => ({
translate: (key: string) => key,
}),
}))
describe('ComponentName', () => {
beforeEach(() => {
jest.clearAllMocks()
})
afterEach(() => {
jest.restoreAllMocks()
})
describe('GIVEN the component is rendered', () => {
describe('WHEN in default state', () => {
it('THEN should display the main container', () => {
render(<ComponentName />)
expect(screen.getByTestId(COMPONENT_NAME_TEST_ID)).toBeInTheDocument()
})
it('THEN should display the title', () => {
render(<ComponentName />)
expect(screen.getByTestId(COMPONENT_NAME_TITLE_TEST_ID)).toBeInTheDocument()
})
})
describe('WHEN user interacts with the form', () => {
it('THEN should enable submit button after valid input', async () => {
const user = userEvent.setup()
render(<ComponentName />)
const input = screen.getByTestId(COMPONENT_NAME_INPUT_TEST_ID)
await user.type(input, 'valid value')
const submitButton = screen.getByTestId(COMPONENT_NAME_SUBMIT_BUTTON_TEST_ID)
expect(submitButton).not.toBeDisabled()
})
})
})
describe('GIVEN the component receives props', () => {
describe('WHEN isLoading is true', () => {
it('THEN should display loading state', () => {
render(<ComponentName isLoading={true} />)
expect(screen.getByTestId(COMPONENT_LOADING_SKELETON_TEST_ID)).toBeInTheDocument()
})
})
})
})
MANDATORY: All test descriptions MUST follow this pattern:
GIVEN or WHEN (always UPPERCASE)THEN (always UPPERCASE)Pattern:
describe('GIVEN [precondition]', () => {
describe('WHEN [action or state]', () => {
it('THEN should [expected outcome]', () => {
// test implementation
})
})
})
Examples:
// Correct
describe('GIVEN the user is logged in', () => {
describe('WHEN clicking the logout button', () => {
it('THEN should redirect to login page', () => { ... })
it('THEN should clear the session', () => { ... })
})
})
// Incorrect - don't do this
describe('Given the user is logged in', () => { ... }) // Wrong case
describe('When clicking the logout button', () => { ... }) // Wrong case
it('Then should redirect', () => { ... }) // Wrong case
it('should redirect', () => { ... }) // Missing THEN
IMPORTANT: When you have multiple tests that follow the same pattern but with different inputs/outputs, use it.each to reduce code duplication and improve maintainability.
When to use it.each:
Pattern for checking multiple elements are displayed:
describe('WHEN the component renders', () => {
it.each([
['name input field', COMPONENT_NAME_INPUT_TEST_ID],
['email input field', COMPONENT_EMAIL_INPUT_TEST_ID],
['cancel button', COMPONENT_CANCEL_BUTTON_TEST_ID],
['submit button', COMPONENT_SUBMIT_BUTTON_TEST_ID],
])('THEN should display the %s', (_, testId) => {
render(<ComponentName />)
expect(screen.getByTestId(testId)).toBeInTheDocument()
})
})
Pattern for checking default values:
describe('WHEN form fields are empty', () => {
it.each([
['name', COMPONENT_NAME_INPUT_TEST_ID],
['email', COMPONENT_EMAIL_INPUT_TEST_ID],
['phone', COMPONENT_PHONE_INPUT_TEST_ID],
])('THEN should have empty %s input by default', (_, testId) => {
render(<ComponentName />)
const container = screen.getByTestId(testId)
const input = container.querySelector('input')
expect(input).toHaveValue('')
})
})
Pattern for testing multiple validation cases:
describe('WHEN user enters invalid data', () => {
it.each([
['empty email', '', 'Email is required'],
['invalid email format', 'invalid', 'Invalid email format'],
['email too long', 'a'.repeat(256) + '@test.com', 'Email too long'],
])('THEN should show error for %s', async (_, inputValue, expectedError) => {
const user = userEvent.setup()
render(<ComponentName />)
const input = screen.getByTestId(COMPONENT_EMAIL_INPUT_TEST_ID)
await user.type(input, inputValue)
await user.tab() // trigger blur validation
expect(screen.getByText(expectedError)).toBeInTheDocument()
})
})
Pattern with object parameters for complex cases:
describe('WHEN displaying different states', () => {
it.each([
{ status: 'pending', expectedColor: 'yellow', expectedText: 'In Progress' },
{ status: 'completed', expectedColor: 'green', expectedText: 'Done' },
{ status: 'failed', expectedColor: 'red', expectedText: 'Error' },
])('THEN should display $status state correctly', ({ status, expectedColor, expectedText }) => {
render(<StatusBadge status={status} />)
const badge = screen.getByTestId(STATUS_BADGE_TEST_ID)
expect(badge).toHaveClass(expectedColor)
expect(badge).toHaveTextContent(expectedText)
})
})
When NOT to use it.each:
This is the most important rule for test selectors. NEVER, under ANY circumstances, use translation keys to find or verify elements.
// ❌ WRONG - NEVER DO THIS
expect(screen.getByText('text_17440321235444hcxi31f8j6')).toBeInTheDocument()
expect(screen.getByText(translate('some_key'))).toBeInTheDocument()
expect(screen.queryByText('text_6271200984178801ba8bdeb2')).toBeInTheDocument()
expect(screen.getAllByText('text_64d23a81a7d807f8aa570509').length).toBeGreaterThan(0)
// ❌ WRONG - Even in waitFor
await waitFor(() => {
expect(screen.getByText('text_6271200984178801ba8bdf7f')).toBeInTheDocument()
})
// ❌ WRONG - Even for error messages
expect(screen.getByText('text_6271200984178801ba8bdf58')).toBeInTheDocument()
// ✅ CORRECT - Always use data-test IDs
expect(screen.getByTestId(COMPONENT_NAME_TITLE_TEST_ID)).toBeInTheDocument()
expect(screen.getByTestId(COMPONENT_ERROR_MESSAGE_TEST_ID)).toBeInTheDocument()
// ✅ CORRECT - For checking element existence
expect(screen.getByTestId(COMPONENT_SUBMIT_BUTTON_TEST_ID)).toBeInTheDocument()
// ✅ CORRECT - For checking element is NOT present
expect(screen.queryByTestId(COMPONENT_ERROR_TEST_ID)).not.toBeInTheDocument()
// ✅ CORRECT - In waitFor
await waitFor(() => {
expect(screen.getByTestId(COMPONENT_SUCCESS_MESSAGE_TEST_ID)).toBeInTheDocument()
})
text_6271200984178801ba8bdeb2 tells you nothing about what's being testedWEBHOOK_FORM_ERROR_MESSAGE_TEST_ID clearly describes the element// ✅ CORRECT - Get element by test ID, then check it exists
const errorMessage = screen.getByTestId(COMPONENT_ERROR_MESSAGE_TEST_ID)
expect(errorMessage).toBeInTheDocument()
// ✅ CORRECT - If you MUST check text content, use toHaveTextContent with the actual translated string
// But prefer checking for element existence via data-test ID
// ✅ CORRECT - Find input within data-test container, use type assertion (NOT non-null assertion)
const inputContainer = screen.getByTestId(COMPONENT_NAME_INPUT_TEST_ID)
const input = inputContainer.querySelector('input') as HTMLInputElement
await user.type(input, 'test value')
// ❌ WRONG - Don't use non-null assertion
// const input = inputContainer.querySelector('input')
// await user.type(input, 'test value') // ESLint error if using non-null assertion
Always use as HTMLInputElement (or appropriate HTML type) instead of non-null assertion:
// ✅ CORRECT - Use type assertion
const input = container.querySelector('input') as HTMLInputElement
const button = container.querySelector('button') as HTMLButtonElement
const select = container.querySelector('select') as HTMLSelectElement
// ❌ WRONG - Non-null assertion causes ESLint errors
// const input = container.querySelector('input') with non-null assertion
// await user.type(input, 'value') // Fails: typescript-eslint/no-non-null-assertion
Step 3.6.1: Check for existing mocks
Before creating new mocks, search the codebase:
// Search in existing test files for similar mocks
grep -r "mockInvoice" src/**/__tests__/*.tsx
grep -r "createMock" src/**/__tests__/*.tsx
Step 3.6.2: Refactor shared mocks
If you find the same mock used in multiple test files, move it to a shared location:
src/__mocks__/ or src/test-utils/mocks/Shared mock pattern:
// src/__mocks__/invoiceMocks.ts
import { CurrencyEnum, InvoiceStatusTypeEnum } from '~/generated/graphql'
export const createMockInvoice = (overrides = {}) => ({
id: 'invoice-1',
status: InvoiceStatusTypeEnum.Finalized,
currency: CurrencyEnum.Usd,
totalAmountCents: 10000,
...overrides,
})
export const createMockCustomer = (overrides = {}) => ({
id: 'customer-1',
name: 'Test Customer',
...overrides,
})
Usage in tests:
import { createMockCustomer, createMockInvoice } from '~/mocks/invoiceMocks'
const mockInvoice = createMockInvoice({ status: InvoiceStatusTypeEnum.Draft })
# Run coverage ONLY on the new/changed files from the PR
pnpm test:coverage -- --collectCoverageFrom='src/path/to/new-file.tsx' src/path/to/__tests__/new-file.test.tsx
When reviewing coverage results, for each uncovered line/branch ask:
Is this code worth testing?
What would a test for this code look like?
expect(component).toBeInTheDocument() with no real assertion → Skip itWould adding this test improve confidence in the code?
Minimum: 80% on new code. Aim higher for critical files:
| Scenario | Minimum Coverage | Target Coverage | Reason | | -------------------------------- | ---------------- | --------------- | ------------------------------------- | | Custom hooks | 85% | 95-100% | Core contracts, always testable | | Utility/helper functions | 85% | 95-100% | Pure logic, easy to test | | Complex business logic | 80% | 90-100% | High value, many edge cases | | Form with validation | 80% | 85-95% | Test validation + submission + errors | | Component with conditional logic | 75% | 85-90% | Test all branches and interactions | | Simple component with some logic | 65% | 75-85% | Test the logic paths | | Mostly presentational component | 50% | 60-70% | Test meaningful interactions only |
IMPORTANT: Do NOT add coverage note comments to test files. Test files should contain only tests, mocks, and necessary setup code.
// DON'T DO THIS
/**
* Coverage Note: This test file achieves ~65% coverage...
* Uncovered code includes: ...
*/
The coverage targets in Step 4.3 are guidelines. If coverage is lower because the untested code is trivial, that's fine - no documentation needed.
pnpm test src/path/to/__tests__/file.test.tsx
CRITICAL: After all tests pass, you MUST run the code style check:
pnpm run code:style
Rules:
pnpm run code:style after fixing to confirm all ERRORs are resolvedexpect.objectContaining() for partial matchingpnpm test <test-file>pnpm run code:style (MANDATORY - fix ERRORs only, ignore warnings)Before completing the task, you MUST verify that NO translation keys exist in the test files.
Run this check on all test files created or modified:
# Search for translation key patterns in test files (text_ followed by alphanumeric characters)
grep -E "text_[a-zA-Z0-9]+" src/path/to/__tests__/*.test.tsx
If ANY translation keys are found, you MUST remove them:
title: 'text_xxx' with title: expect.any(String) or remove entirelydescription: 'text_xxx' with description: expect.any(String) or remove entirelyactionText: 'text_xxx' with actionText: expect.any(String) or remove entirelymessage: 'text_xxx' with removal (only keep severity for toasts)screen.getByText('text_xxx') with screen.getByTestId(COMPONENT_TEST_ID)This check is NON-NEGOTIABLE. Tests with translation keys will break when translations change.
| New Code Type | Recommended Action | Minimum Coverage | | ----------------------------------- | ------------------------------------- | ---------------- | | Custom hooks | Full test coverage (contract + edges) | 85% | | Utility/helper functions | Full test coverage (all paths) | 85% | | Complex logic with branches | Full test coverage | 80% | | Form handling + validation | Test validation + submission + errors | 80% | | Component with conditional logic | Test all branches and interactions | 75% | | Simple component with some logic | Test the logic paths | 65% | | Mostly presentational + minor logic | Test meaningful interactions | 50% |
Files that get NO tests (Explicit Exclusions only):
| Exclusion Type | Example |
| --------------------------- | -------------------------------- |
| Pure type definitions | types.ts, *.d.ts |
| Auto-generated files | generated/graphql.tsx |
| Translation files | translations/base.json |
| Pure CSS/SCSS | styles.css |
| Barrel/index re-exports | index.ts with only exports |
| Static constants (no logic) | constants.ts with plain values |
Use snapshot tests where they add value, but don't force them.
Good candidates for snapshots:
NOT good for snapshots:
Snapshot test pattern:
describe('GIVEN the component renders different states', () => {
describe('WHEN in default state', () => {
it('THEN should match snapshot', () => {
const { container } = render(<Component />)
expect(container).toMatchSnapshot()
})
})
describe('WHEN in error state', () => {
it('THEN should match snapshot', () => {
const { container } = render(<Component error="Something failed" />)
expect(container).toMatchSnapshot()
})
})
})
Important: If a component has dynamic content (dates, IDs), either:
expect.any(String) insteaddescribe('GIVEN the component is loading', () => {
describe('WHEN data is being fetched', () => {
it('THEN should display loading skeleton', () => {
render(<Component isLoading={true} />)
expect(screen.getByTestId(COMPONENT_LOADING_SKELETON_TEST_ID)).toBeInTheDocument()
})
it('THEN should not display content', () => {
render(<Component isLoading={true} />)
expect(screen.queryByTestId(COMPONENT_CONTENT_TEST_ID)).not.toBeInTheDocument()
})
})
})
describe('GIVEN an error occurred', () => {
describe('WHEN the error is displayed', () => {
it('THEN should show error message', () => {
render(<Component error="Something went wrong" />)
expect(screen.getByTestId(COMPONENT_ERROR_TEST_ID)).toBeInTheDocument()
})
})
})
describe('GIVEN the form is filled', () => {
describe('WHEN user submits the form', () => {
it('THEN should call the submit handler with form values', async () => {
const onSubmit = jest.fn()
const user = userEvent.setup()
render(<FormComponent onSubmit={onSubmit} />)
const nameInput = screen.getByTestId(FORM_NAME_INPUT_TEST_ID).querySelector('input') as HTMLInputElement
await user.type(nameInput, 'Test Name')
const submitButton = screen.getByTestId(FORM_SUBMIT_BUTTON_TEST_ID)
await user.click(submitButton)
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith(
expect.objectContaining({ name: 'Test Name' })
)
})
})
})
})
describe('GIVEN the feature flag is enabled', () => {
describe('WHEN component renders', () => {
it('THEN should display the new feature', () => {
render(<Component featureEnabled={true} />)
expect(screen.getByTestId(COMPONENT_NEW_FEATURE_TEST_ID)).toBeInTheDocument()
})
})
})
describe('GIVEN the feature flag is disabled', () => {
describe('WHEN component renders', () => {
it('THEN should not display the new feature', () => {
render(<Component featureEnabled={false} />)
expect(screen.queryByTestId(COMPONENT_NEW_FEATURE_TEST_ID)).not.toBeInTheDocument()
})
})
})
import { Settings } from 'luxon'
describe('ComponentWithDates', () => {
const originalDefaultZone = Settings.defaultZone
beforeAll(() => {
Settings.defaultZone = 'UTC'
})
afterAll(() => {
Settings.defaultZone = originalDefaultZone
})
// ... tests
})
describe('GIVEN the mutation succeeds', () => {
describe('WHEN user performs action', () => {
it('THEN should show success toast', async () => {
const user = userEvent.setup()
render(<Component />, { mocks: successMocks })
const actionButton = screen.getByTestId(COMPONENT_ACTION_BUTTON_TEST_ID)
await user.click(actionButton)
// ✅ CORRECT - Use expect.objectContaining to skip translation key verification
await waitFor(() => {
expect(addToast).toHaveBeenCalledWith(
expect.objectContaining({ severity: 'success' })
)
})
// ❌ WRONG - NEVER include translation keys in assertions
// await waitFor(() => {
// expect(addToast).toHaveBeenCalledWith({
// message: 'text_6271200984178801ba8bdf7f', // ❌ NEVER DO THIS
// severity: 'success',
// })
// })
})
})
})
When testing that a dialog or modal was opened with specific properties, NEVER include translation keys for title, description, or actionText. Instead, verify only non-translation properties like colorVariant, or use expect.any(String) if you need to verify the presence of text fields.
describe('GIVEN user wants to delete an item', () => {
describe('WHEN clicking the delete button', () => {
it('THEN should open confirmation dialog with danger variant', async () => {
const user = userEvent.setup()
render(<Component />)
const deleteButton = screen.getByTestId(COMPONENT_DELETE_BUTTON_TEST_ID)
await user.click(deleteButton)
// ✅ CORRECT - Only verify non-translation key properties
expect(mockDialogOpen).toHaveBeenCalledWith(
expect.objectContaining({
colorVariant: 'danger',
}),
)
// ✅ ALSO CORRECT - If you need to verify title/description exist, use expect.any(String)
expect(mockDialogOpen).toHaveBeenCalledWith(
expect.objectContaining({
title: expect.any(String),
description: expect.any(String),
colorVariant: 'danger',
}),
)
// ❌ WRONG - NEVER include translation keys in dialog assertions
// expect(mockDialogOpen).toHaveBeenCalledWith(
// expect.objectContaining({
// title: 'text_6271200984178801ba8bdeb2', // ❌ NEVER DO THIS
// description: 'text_6271200984178801ba8bded2', // ❌ NEVER DO THIS
// actionText: 'text_6271200984178801ba8bdf0c', // ❌ NEVER DO THIS
// colorVariant: 'danger',
// }),
// )
})
})
})
Invoke this skill with a PR number or branch name:
/make-tests #123
/make-tests 123
/make-tests feature/my-new-feature
/make-tests origin/fix/bug-123
/make-tests chore/update-deps
/make-tests #456 # Analyze PR #456
/make-tests feature/add-webhook-form # Analyze local branch
/make-tests origin/feature/new-dialog # Analyze remote branch
The skill will analyze the PR or branch, identify files needing tests (compared to main), and create comprehensive tests following the BDD approach and project conventions.
Remember: NEVER use translation keys in tests. Always use data-test IDs.
development
Migrate a React form from Formik to TanStack Form following project conventions. Use this skill when the user wants to migrate a form component from Formik to TanStack Form.
development
Migrate a dialog component from the legacy imperative ref-based Dialog system to the new hook-based NiceModal dialog system (FormDialog, CentralizedDialog, or FormDialogOpeningDialog). This skill focuses only on the migration — testing is handled separately.
development
Create Cypress e2e tests for a specific feature. Accepts a feature name, PR number, or branch name. Navigates the codebase, adds data-test attributes if missing, writes happy-path tests following project conventions, and validates them with Cypress.
research
Execute git commit with conventional commit message analysis, intelligent staging, and message generation. Use when user asks to commit changes, create a git commit, or mentions "/commit". Supports: (1) Auto-detecting type and scope from changes, (2) Generating conventional commit messages from diff, (3) Interactive commit with optional type/scope/description overrides, (4) Intelligent file staging for logical grouping