agentleFS
LongJohnSilver1504LongJohnSilver1504/claude-skills/skills/frontend-testing/SKILL.md

frontend-testing

Skill1 starsChanged 58 days agoMITView on GitHub

Write behavioral tests for feature modules using Vitest, React Testing Library, and MSW v2. Use when writing tests, adding test coverage, setting up test infrastructure (MSW handlers, shared factories, jsdom polyfills), or when the user says "write tests", "add coverage", "test this component/hook/feature". NOT for judging existing test quality inside the pipeline (the test-reviewer agent owns that gate) or finding the root cause of a failing test (systematic-debugging).

18 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use backend-code-review, frontend-query-mutation and 3 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • jpoindexterSkill

    backend-code-review

    Review backend code for quality, security, maintainability, and best practices based on established checklist rules. Use when the user requests a review, analysis, or improvement of backend files (e.g., `.py`) under the `api/` directory. Do NOT use for frontend files (e.g., `.tsx`, `.ts`, `.js`). Supports pending-change review, code snippets review, and file-focused review.

    Also in 5 projects that use this

  • lien0219Skill

    frontend-query-mutation

    Guide for implementing Langflow frontend query and mutation patterns with Axios and TanStack React Query v5. Trigger when creating or updating API hooks in controllers/API/queries, consuming UseRequestProcessor in components, deciding whether to use useQuery or useMutation, handling conditional queries, cache invalidation, mutation error handling, or migrating legacy API calls to the query hook pattern.

    Also in 3 projects that use this

  • Charlie85270Skill

    component-refactoring

    Refactor high-complexity React components in Dify frontend. Use when `pnpm analyze-component --json` shows complexity > 50 or lineCount > 300, when the user asks for code splitting, hook extraction, or complexity reduction, or when `pnpm analyze-component` warns to refactor before testing; avoid for simple/well-structured components, third-party wrappers, or when the user explicitly wants testing without refactoring.

    Also in 4 projects that use this

  • chendongqiSkill

    frontend-code-review

    Trigger when the user requests a review of frontend files (e.g., `.tsx`, `.ts`, `.js`). Support both pending-change reviews and focused file reviews while applying the checklist rules.

    Also in 7 projects that use this

  • etewiahSkill

    e2e-testing

    Playwright E2E testing and Lighthouse performance auditing. Use when setting up E2E tests, running Playwright tests, performing Lighthouse audits, or debugging E2E test failures.

    Also in 3 projects that use this

The file

Show its rules and text

We split this skill into the rules it gives an agent

15 rules. 0 are common, 9 are distinctive, and 6 are very specific to this project.

  • 0

    Common

    Most of the 18 edited versions we read keep these.

  • 9

    Distinctive

    This author's own choices. Worth a look.

  • 6

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Write tests that give confidence your app works — not tests that verify implementation details.

    line 9
  2. Distinctive:

    Do NOT write all the tests first and then all the implementation.

    line 64
  3. Distinctive:

    Follow AAA pattern: Arrange, Act, Assert

    line 95
  4. Distinctive:

    Always prefer queries that match how users and assistive technology interact with your app.

    line 99
  5. Distinctive:

    When a label is not available (less ideal than label text).

    line 131
  6. Distinctive:

    Only use when semantic queries don't work or don't make sense (e.g., dynamic text, non-semantic wrappers).

    line 146
  7. Very specific:

    When a test needs a *different* router shape (e.g., populated query), keep the mock's mutable pieces at module scope and reset them in …

    line 210
  8. Distinctive:

    No mocking needed. Test inputs → outputs.

    line 253
  9. Very specific:

    When testing async behavior, use waitFor for:

    line 293
  10. Very specific:

    Always await userEvent methods

    line 353
  11. Very specific:

    Use waitFor or findBy* for async assertions

    line 354
  12. Distinctive:

    If you see act() warnings, your test is incomplete

    line 355
  13. Distinctive:

    Read references/factories.md for the full patterns (factory templates, MSW wiring, mapper tests, contract smoke) before writing factories …

    line 367
  14. Very specific:

    Read the feature's existing artifacts (.claude/pipeline/{feature}/ and the feature folder: DESIGN.md, PRD.md, UX-spec.md, the …

    line 384
  15. Very specific:

    Use the AskUserQuestion tool for the transition (never numbered text options), with a one-line summary of what was completed.

    line 388

“Common” is measured: kept by at least 6 in 10 of the 18 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Project config: {app} is the project's new-code root from .claude/rules/project-structure.md; project values (package manager, commands, base branch, error surface) come from docs/agents/project-conventions.md. Resolve both before writing any file — never assume. If a needed file is missing, stop: "Run /setup-daher-skills first — missing <file>."

Frontend Testing

Write tests that give confidence your app works — not tests that verify implementation details.

Testing Philosophy

"The more your tests resemble the way your software is used, the more confidence they can give you." — Kent C. Dodds

Do NOT test implementation details

Implementation details are things users of your code will not typically use, see, or know about. Testing them leads to:

  • False negatives — tests break when you refactor, even though the app still works
  • False positives — tests pass even though the app is broken
// ❌ DON'T: Test hook return values directly
const { result } = renderHook(() => useNavbar())
expect(result.current.isOpen).toBe(false)  // user never sees "isOpen"

// ✅ DO: Test what the user sees and does
render(<Navbar />)
await userEvent.click(screen.getByLabelText(/menu/i))
expect(screen.getByRole('navigation')).toBeVisible()

Who are the "users"?

  1. End users — interact via rendered UI (buttons, text, navigation)
  2. Developers — interact via props and public hook APIs

Tests should only assert on things these two users care about.

When renderHook IS appropriate

  • Provider/context hooks (useAuth) — the hook IS the public developer API
  • TanStack Query hooks — testing data fetching behavior
  • Pure logic hooks with no UI counterpart (rare)

For hooks that power a component (useNavbar, useBackHeader, useLanguageSelector), test through the component instead.

Stack

ToolPurpose
VitestTest runner and assertions
React Testing LibraryComponent and hook rendering
@testing-library/user-eventSimulating user interactions
MSW v2API mocking at the network level
jsdomBrowser environment simulation

Test-First Workflow

Build features as vertical slices with a red-green loop — one test, one piece of implementation, repeat.

Anti-pattern: horizontal slices

Do NOT write all the tests first and then all the implementation. Tests written in bulk verify imagined behavior — they end up asserting on the shape of things (data structures, signatures) instead of user-facing behavior, pass when behavior breaks, and fail when it's fine.

WRONG (horizontal):            RIGHT (vertical, tracer bullets):
  RED:   test1..test5            RED→GREEN: test1→impl1
  GREEN: impl1..impl5            RED→GREEN: test2→impl2
                                 RED→GREEN: test3→impl3

Tracer bullet first

Start with ONE test that proves the path end-to-end — component renders → hook fires → MSW handler answers → UI shows the result — then write the minimal code to pass it. Each subsequent test responds to what the previous cycle taught you: because you just wrote the code, you know exactly which behavior matters and how to verify it.

The loop

  1. RED — write the next failing test and watch it fail: pnpm vitest run {features-root}/{feature} (or pnpm test:watch while looping — a long-lived process: stop it when the loop ends, and prefer vitest run in agent sessions)
  2. GREEN — write the minimal code that passes. No speculative features, no code for tests you haven't written yet.
  3. REFACTOR — clean up with tests green. Never refactor while RED.

Mock only at system boundaries: HTTP via MSW handlers (see references/msw-setup.md), next/router, time. Never mock your own modules just to force a test green — see references/testing-anti-patterns.md.

Regression tests must fail first

A bug-fix test only counts if you have verified it fails without the fix. If it passes before the fix lands, it isn't testing the bug.

Test File Conventions

Co-location (tests live next to their source file, never in separate test directories), runner config, test commands, and per-feature helper locations are defined in the project rule — see .claude/rules/testing.md. Beyond the rule:

  • One describe block per module, nested describes for scenarios
  • Follow AAA pattern: Arrange, Act, Assert

Query Priority (Accessibility First)

Always prefer queries that match how users and assistive technology interact with your app. Follow this priority order:

1. 🥇 getByRole (Preferred)

Queries elements exposed in the accessibility tree. Best for testing accessibility and should be your default choice.

// Buttons
screen.getByRole('button', { name: /submit/i })
screen.getByRole('button', { name: /cancel/i })

// Form fields
screen.getByRole('textbox', { name: /email/i })
screen.getByRole('textbox', { name: /password/i })
screen.getByRole('combobox', { name: /country/i })

// Navigation
screen.getByRole('navigation')
screen.getByRole('link', { name: /home/i })

// Headings
screen.getByRole('heading', { name: /dashboard/i, level: 1 })

2. 🥈 getByLabelText

For form fields associated with labels. Excellent for accessibility.

screen.getByLabelText(/email address/i)
screen.getByLabelText(/password/i)

3. 🥉 getByPlaceholderText

When a label is not available (less ideal than label text).

screen.getByPlaceholderText(/search\.\.\./i)

4. getByText

For non-interactive elements (paragraphs, divs, spans) or finding by text content.

screen.getByText(/welcome back/i)
screen.getByText(/no results found/i)

5. 🚫 getByTestId (Last Resort)

Only use when semantic queries don't work or don't make sense (e.g., dynamic text, non-semantic wrappers).

// Avoid this when possible
screen.getByTestId('user-avatar')

Why this order matters:

  • getByRole and getByLabelText test accessibility
  • They force proper semantic HTML
  • They align with how screen readers work
  • They make your app more accessible to users with disabilities

Testing Strategy by Layer

1. Components (Primary — test behavior through UI)

This is where most tests should live. Render the component, interact as a user would, assert on visible output.

import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'

// vi.mock is HOISTED to the top of the file — declare it at module level,
// never inside an `it` block. Module-scope mock fns are safe to reference
// because the factory's returned functions read them at render time.
const mockPush = vi.fn()

vi.mock('next/router', () => ({
  useRouter: () => ({ push: mockPush, isReady: true, query: {} }),
}))

describe('Navbar', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('opens the menu when hamburger is clicked', async () => {
    render(<Navbar />)

    // Use getByRole for buttons with accessible name
    await userEvent.click(screen.getByRole('button', { name: /menu/i }))

    expect(screen.getByRole('navigation')).toBeVisible()
  })

  it('hides auth-required items when unauthenticated', async () => {
    render(<Navbar />)  // default: unauthenticated

    await userEvent.click(screen.getByRole('button', { name: /menu/i }))

    expect(screen.queryByText(/messages/i)).not.toBeInTheDocument()
  })

  it('navigates to home when logo is clicked', async () => {
    render(<Navbar />)

    await userEvent.click(screen.getByRole('link', { name: /home/i }))

    expect(mockPush).toHaveBeenCalledWith('/')
  })
})

When a test needs a different router shape (e.g., populated query), keep the mock's mutable pieces at module scope and reset them in beforeEach — the same pattern the project's existing tests use:

const mockPush = vi.fn().mockResolvedValue(undefined)
const mockQuery: Record<string, string> = {}

vi.mock('next/router', () => ({
  useRouter: () => ({ isReady: true, query: mockQuery, push: mockPush }),
}))

beforeEach(() => {
  vi.clearAllMocks()
  Object.keys(mockQuery).forEach((key) => delete mockQuery[key])
})

it('preselects the reservation from the URL', () => {
  mockQuery.reservationId = '42'
  // ...
})

2. Providers / Context Hooks (renderHook is OK)

The hook IS the public API for developers. Test it directly.

import { renderHook, waitFor } from '@testing-library/react'
import { AuthProvider, useAuth } from './auth-provider'

const wrapper = ({ children }) => <AuthProvider>{children}</AuthProvider>

describe('useAuth', () => {
  it('returns authenticated when valid user in localStorage', async () => {
    localStorage.setItem('user', JSON.stringify(validUser))
    const { result } = renderHook(() => useAuth(), { wrapper })

    await waitFor(() => expect(result.current.isAuthenticated).toBe(true))
  })
})

3. Domain Layer (Pure Functions)

No mocking needed. Test inputs → outputs.

import { canEdit, getStatusLabel } from '../{feature}.service'

describe('{feature}.service', () => {
  it('returns true for active items', () => {
    expect(canEdit({ status: 'active' })).toBe(true)
  })
})

4. API Layer (MSW)

Use MSW to mock HTTP at the network level. The server is set up per test file — there is no global MSW server (see references/msw-setup.md). Handlers return wire shapes — use DTO factories (see Shared Test Factories below) rather than inline literals for anything non-trivial. Adapter tests then assert on the mapped domain output, exercising the full validate→map boundary (rules/api-boundary.md).

import { setupServer } from 'msw/node'
import { http, HttpResponse } from 'msw'

const server = setupServer()
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

describe('{feature}Api', () => {
  it('maps response correctly', async () => {
    server.use(
      http.get('*/{feature}s', () => HttpResponse.json({ results: [{ id: 1 }] }))
    )
    const result = await {feature}Api.getAll({})
    expect(result.results).toHaveLength(1)
  })
})

Async Testing Patterns

Use waitFor for Async Assertions

When testing async behavior, use waitFor for:

  • Component re-renders
  • State changes after async operations
  • Asynchronous updates from data fetching
import { waitFor } from '@testing-library/react'

it('displays data after loading', async () => {
  render(<UserProfile userId="123" />)

  // Wait for loading to complete
  await waitFor(() => {
    expect(screen.queryByText(/loading/i)).not.toBeInTheDocument()
  })

  // Assert final state
  expect(screen.getByText(/john doe/i)).toBeInTheDocument()
})

Alternative: findBy Queries

For simpler cases, use findBy* queries (combines getBy + waitFor):

it('displays data after loading', async () => {
  render(<UserProfile userId="123" />)

  // Automatically waits for element to appear
  expect(await screen.findByText(/john doe/i)).toBeInTheDocument()
})

Handling act() Warnings

React Testing Library methods handle act() automatically. If you see act() warnings:

What it means:

  • Component state updated but test didn't wait for it
  • Indicates untested component updates

How to fix:

// ❌ BAD: Missing await
userEvent.click(button)
expect(screen.getByText(/success/i)).toBeInTheDocument()

// ✅ GOOD: Await user interaction
await userEvent.click(button)
expect(screen.getByText(/success/i)).toBeInTheDocument()

// ✅ GOOD: Wait for async updates
await userEvent.click(button)
await waitFor(() => {
  expect(screen.getByText(/success/i)).toBeInTheDocument()
})

General rule:

  • Always await userEvent methods
  • Use waitFor or findBy* for async assertions
  • If you see act() warnings, your test is incomplete

Additional Resources

  • For test utilities, wrapper patterns, rendering helpers, and the gotchas that flake tests (QueryClient lifetime, disappearing loaders, error-boundary spies, the jsdom/real-browser boundary, when renderHook is warranted), see references/test-utilities.md
  • For MSW v2 setup, handler patterns, and network mocking, see references/msw-setup.md
  • For the ways tests rot into false confidence (testing mocks, test-only production code, mocking without understanding), see references/testing-anti-patterns.md

Factories, Mapper Tests, and Contract Smoke

When a domain type appears in 3+ test files (or has 5+ required fields), use a shared test factory — two levels, mirroring the API boundary: DTO factories (wire shapes, satisfies z.input<typeof xDtoSchema>, feed MSW handlers) and domain factories (hand-authored domain types, feed component/hook/domain tests). Mappers always get direct unit tests — the cheapest tests in the codebase.

Read references/factories.md for the full patterns (factory templates, MSW wiring, mapper tests, contract smoke) before writing factories or MSW handlers.

Checklist

[ ] Test-first: every test was seen RED before its implementation landed
[ ] Component tests: render + userEvent + screen assertions
[ ] Provider tests: renderHook for public API hooks only
[ ] Domain tests: pure function input/output
[ ] Mapper tests: toX(dtoFactory()) asserts mapping + normalization
[ ] API tests: MSW handlers return DTO-factory output; assert mapped domain result
[ ] DTO factories declare `satisfies z.input<typeof xDtoSchema>`
[ ] NO hook-level tests for hooks that power components

Resume After Context Cleanup

Read the feature's existing artifacts (.claude/pipeline/{feature}/ and the feature folder: DESIGN.md, PRD.md, UX-spec.md, the implementation plan, PROGRESS.md — whichever exist), then the feature code and its existing test files, and continue from where they leave off — never restart from scratch.

Next Step

Use the AskUserQuestion tool for the transition (never numbered text options), with a one-line summary of what was completed. Options:

  • generate-feature-doc — document the implementation
  • Something else — do something different

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.