agentsclimarketplace

Qa react

Skill endorphin-ai/claude-code-teams/go-react-team/.claude/skills/qa-react

React testing skill with React Testing Library, Vitest, MSW, and Playwright. Use when writing or running React frontend tests.From its SKILL.md

Install
npx -y skills add endorphin-ai/claude-code-teams --skill qa-react

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

17.4 KB, ~4.1k tokens by cl100k_base, as published. Nobody here has run it

QA React

Purpose

This skill equips the tester-react agent with everything needed to write, run, and maintain React frontend tests in a fullstack Golang + React codebase. It covers unit tests for components and hooks, integration tests for pages, API mocking with MSW, accessibility checks, and optional E2E flows with Playwright. The guiding philosophy: test what the user experiences, not how the code is wired.

Core Testing Stack

ToolRoleVersion Guidance
VitestTest runnerVite-compatible, ESM-native, fast HMR-aware
React Testing Library (RTL)Component rendering + queries@testing-library/react
user-eventRealistic user interaction simulation@testing-library/user-event v14+
MSWNetwork-level API mockingmsw v2+ — intercepts fetch/axios at Service Worker level
jest-axeAutomated accessibility assertionsjest-axe via toHaveNoViolations
PlaywrightE2E browser tests (optional)For critical user flows only
@testing-library/react-hooksHook testing (legacy)Prefer renderHook from @testing-library/react v14+

RTL Philosophy: Test Behavior, Not Implementation

The single most important principle: tests should resemble how users interact with your app. If a test breaks because you renamed an internal state variable but the UI behavior didn't change, the test is wrong.

Forbidden Patterns

  • Never query by CSS class name, tag name, or component internal state
  • Never assert on useState / useReducer values directly
  • Never use container.querySelector unless absolutely no RTL query works
  • Never test implementation details (internal function calls, hook return values in isolation without rendering)
  • Never use act() directly when RTL's waitFor / findBy handles it

Encouraged Patterns

  • Query by accessible role, label, placeholder, text — what the user sees
  • Simulate real user actions (click, type, tab) with userEvent
  • Assert on visible output: text content, element presence/absence, ARIA attributes
  • Use waitFor and findBy* for async state transitions

RTL Query Priority (strict order)

Use the highest-priority query that works for your case:

  1. getByRole — Accessible role + name. Best for buttons, headings, links, form controls. Example: getByRole('button', { name: /submit/i })
  2. getByLabelText — Form fields with associated <label>. Example: getByLabelText(/email address/i)
  3. getByPlaceholderText — When label is absent (not ideal, but acceptable). Example: getByPlaceholderText(/search/i)
  4. getByText — Visible text content. Example: getByText(/welcome back/i)
  5. getByDisplayValue — Current value of input/textarea/select.
  6. getByAltText — Images with alt text.
  7. getByTitle — Elements with title attribute.
  8. getByTestIdLast resort only. Use data-testid when no semantic query works. Example: getByTestId('chart-container')

Query Variants

NeedPrefixThrows on miss?Async?
Element must existgetByYesNo
Element must NOT existqueryByNo (returns null)No
Element will appear (async)findByYes (waits)Yes
Multiple elementsgetAllBy / queryAllBy / findAllByRespective behaviorRespective

User Event Simulation

Always use @testing-library/user-event over fireEvent. It simulates complete user interaction sequences (focus, keydown, keypress, keyup, input, change) rather than dispatching isolated synthetic events.

import userEvent from '@testing-library/user-event';

// Setup — create a user instance per test
const user = userEvent.setup();

// Click
await user.click(screen.getByRole('button', { name: /save/i }));

// Type into input
await user.type(screen.getByLabelText(/username/i), 'john_doe');

// Clear and retype
await user.clear(screen.getByLabelText(/email/i));
await user.type(screen.getByLabelText(/email/i), '[email protected]');

// Select option
await user.selectOptions(screen.getByRole('combobox', { name: /country/i }), 'US');

// Keyboard
await user.keyboard('{Enter}');
await user.tab();

// Upload file
const file = new File(['content'], 'doc.pdf', { type: 'application/pdf' });
await user.upload(screen.getByLabelText(/upload/i), file);

Async Testing

waitFor — poll until assertion passes

await waitFor(() => {
  expect(screen.getByText(/data loaded/i)).toBeInTheDocument();
});

findBy — shorthand for waitFor + getBy

const heading = await screen.findByRole('heading', { name: /dashboard/i });
expect(heading).toBeInTheDocument();

waitForElementToBeRemoved — wait for loading spinners to disappear

await waitForElementToBeRemoved(() => screen.queryByText(/loading/i));

Rules

  • Default timeout is 1000ms. Increase for slow operations: waitFor(() => ..., { timeout: 3000 })
  • Never use setTimeout or manual delays in tests
  • If a findBy query is sufficient, prefer it over waitFor + getBy

MSW (Mock Service Worker) — API Mocking

MSW intercepts HTTP requests at the network level. Tests hit the same fetch/axios calls as production — no mocking of modules.

Server Setup (src/test/setup.ts)

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

// Default handlers — shared across all tests
export const handlers = [
  http.get('/api/v1/user/me', () => {
    return HttpResponse.json({ id: 1, name: 'Test User', email: '[email protected]' });
  }),
  http.get('/api/v1/health', () => {
    return HttpResponse.json({ status: 'ok' });
  }),
];

export const server = setupServer(...handlers);

// Vitest global setup
beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

Per-Test Handler Overrides

import { server } from '@/test/setup';
import { http, HttpResponse } from 'msw';

test('shows error when API fails', async () => {
  server.use(
    http.get('/api/v1/user/me', () => {
      return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 });
    }),
  );

  render(<ProfilePage />);
  expect(await screen.findByText(/unauthorized/i)).toBeInTheDocument();
});

MSW Handler Patterns

ScenarioPattern
Success responseHttpResponse.json(data, { status: 200 })
Error responseHttpResponse.json({ error: msg }, { status: 4xx/5xx })
Empty responseHttpResponse.json(null, { status: 204 })
Network errorHttpResponse.error()
Delayed responseawait delay(ms) before returning
Request body validationRead await request.json() and assert / branch
Paginated listRead URL search params, return sliced data

MSW Convention

  • Place default handlers in src/test/handlers/ organized by API domain (e.g., user.ts, projects.ts, auth.ts)
  • Export each handler array, compose into server in setup.ts
  • Override in individual tests only for error/edge cases

Component Testing Pattern

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

describe('LoginForm', () => {
  const user = userEvent.setup();
  const mockOnSubmit = vi.fn();

  beforeEach(() => {
    mockOnSubmit.mockClear();
  });

  it('renders email and password fields', () => {
    render(<LoginForm onSubmit={mockOnSubmit} />);

    expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
    expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /sign in/i })).toBeInTheDocument();
  });

  it('calls onSubmit with credentials when form is filled and submitted', async () => {
    render(<LoginForm onSubmit={mockOnSubmit} />);

    await user.type(screen.getByLabelText(/email/i), '[email protected]');
    await user.type(screen.getByLabelText(/password/i), 'secret123');
    await user.click(screen.getByRole('button', { name: /sign in/i }));

    expect(mockOnSubmit).toHaveBeenCalledWith({
      email: '[email protected]',
      password: 'secret123',
    });
  });

  it('shows validation error when email is empty', async () => {
    render(<LoginForm onSubmit={mockOnSubmit} />);

    await user.click(screen.getByRole('button', { name: /sign in/i }));

    expect(await screen.findByText(/email is required/i)).toBeInTheDocument();
    expect(mockOnSubmit).not.toHaveBeenCalled();
  });
});

Hook Testing Pattern

Use renderHook from @testing-library/react (not the deprecated @testing-library/react-hooks package).

import { renderHook, waitFor } from '@testing-library/react';
import { useCounter } from './useCounter';

describe('useCounter', () => {
  it('starts with initial value', () => {
    const { result } = renderHook(() => useCounter(5));
    expect(result.current.count).toBe(5);
  });

  it('increments count', () => {
    const { result } = renderHook(() => useCounter(0));
    act(() => result.current.increment());
    expect(result.current.count).toBe(1);
  });
});

Hooks that need providers (React Query, Router, etc.)

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

function createWrapper() {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  });
  return ({ children }: { children: React.ReactNode }) => (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
}

it('fetches user data', async () => {
  const { result } = renderHook(() => useUser(1), { wrapper: createWrapper() });

  await waitFor(() => expect(result.current.isSuccess).toBe(true));
  expect(result.current.data?.name).toBe('Test User');
});

Page / Integration Testing Pattern

Page tests render a full page component with router context, mocked API, and all providers.

import { render, screen } from '@/test/test-utils'; // custom render with providers
import { DashboardPage } from './DashboardPage';

describe('DashboardPage', () => {
  it('loads and displays project list', async () => {
    render(<DashboardPage />);

    // Wait for loading to complete
    await waitForElementToBeRemoved(() => screen.queryByText(/loading/i));

    // Assert data is rendered
    expect(screen.getByRole('heading', { name: /my projects/i })).toBeInTheDocument();
    expect(screen.getAllByRole('listitem')).toHaveLength(3);
  });

  it('navigates to project detail when clicked', async () => {
    render(<DashboardPage />);

    await waitForElementToBeRemoved(() => screen.queryByText(/loading/i));
    await user.click(screen.getByText(/project alpha/i));

    expect(await screen.findByRole('heading', { name: /project alpha/i })).toBeInTheDocument();
  });
});

Custom Render (src/test/test-utils.tsx)

import { render, RenderOptions } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

function AllProviders({ children }: { children: React.ReactNode }) {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  });

  return (
    <QueryClientProvider client={queryClient}>
      <BrowserRouter>{children}</BrowserRouter>
    </QueryClientProvider>
  );
}

const customRender = (ui: React.ReactElement, options?: Omit<RenderOptions, 'wrapper'>) =>
  render(ui, { wrapper: AllProviders, ...options });

export * from '@testing-library/react';
export { customRender as render };

Snapshot Testing

Use snapshots sparingly and only for stable, presentational UI that changes rarely.

it('matches snapshot for static footer', () => {
  const { container } = render(<Footer />);
  expect(container.firstChild).toMatchSnapshot();
});

Snapshot Rules

  • Never snapshot components with dynamic data (dates, IDs, random values)
  • Never snapshot entire pages
  • Prefer inline snapshots (toMatchInlineSnapshot) for small outputs
  • When a snapshot update is needed, review the diff carefully before accepting with --update
  • If a snapshot breaks frequently, replace it with explicit assertions

Accessibility Testing

import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

it('has no accessibility violations', async () => {
  const { container } = render(<LoginForm onSubmit={vi.fn()} />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

Accessibility Checks to Include

  • Run jest-axe on every form component and every page-level component
  • Verify focus management after modal open/close
  • Verify keyboard navigation for custom interactive elements
  • Check ARIA labels on icon-only buttons

Coverage Targets

CategoryTargetRationale
Business logic hooks (use*)80%+Core logic, high ROI
Form components80%+User input paths must be solid
Page components70%+Integration coverage, some paths are hard to reach
Presentational/layout components50%+Low logic, snapshot may suffice
Utility functions90%+Pure functions, easy to test exhaustively
E2E critical paths100% of defined flowsIf you write an E2E test, it must pass

Run coverage: npx vitest run --coverage

Playwright E2E (Optional — Critical Flows Only)

Use Playwright only for high-value end-to-end user flows that cannot be adequately tested with RTL + MSW. Examples: login flow, checkout, multi-step wizard.

import { test, expect } from '@playwright/test';

test('user can log in and see dashboard', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel(/email/i).fill('[email protected]');
  await page.getByLabel(/password/i).fill('password123');
  await page.getByRole('button', { name: /sign in/i }).click();

  await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible();
});

Playwright Rules

  • E2E tests live in e2e/ at project root, not alongside components
  • E2E tests run against a real dev server (or preview build), not mocked APIs
  • Keep E2E suite small: 10-20 critical flows max
  • Tag with @critical for CI gating

Test Setup File (src/test/setup.ts)

This file is loaded by Vitest before every test file. It configures:

  1. MSW server — starts, resets between tests, closes after suite
  2. jest-dom matchers — extends expect with .toBeInTheDocument(), .toHaveTextContent(), etc.
  3. jest-axe matchers — extends expect with .toHaveNoViolations()
  4. Global cleanup — ensures no leaked state between tests
import '@testing-library/jest-dom/vitest';
import { server } from './handlers';

beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

Referenced in vitest.config.ts:

export default defineConfig({
  test: {
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    globals: true,
    css: false,
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      include: ['src/**/*.{ts,tsx}'],
      exclude: ['src/test/**', 'src/**/*.d.ts', 'src/main.tsx'],
    },
  },
});

File Conventions

ConventionRule
Test file locationSame directory as source: Button.tsx -> Button.test.tsx
Test file namingComponentName.test.tsx for components, useHook.test.ts for hooks
Test utility locationsrc/test/test-utils.tsx for custom render, src/test/handlers/ for MSW handlers
E2E test locatione2e/ at project root
Describe blocksdescribe('ComponentName', () => { ... }) — one per file, named after component
Test namesStart with verb describing user action or state: "renders...", "shows...", "calls...", "navigates..."

Conventions

  1. One test file per source file — never combine tests for multiple components
  2. Arrange-Act-Assert — every test follows this structure clearly
  3. No test interdependence — each test must work in isolation; no shared mutable state
  4. Mock at the network level — use MSW, not vi.mock() for API calls. Reserve vi.mock() for non-HTTP dependencies (e.g., window.matchMedia, IntersectionObserver)
  5. Descriptive test names — a failing test name should tell you what broke without reading the test body
  6. No magic numbers — use named constants or factory functions for test data
  7. Test data factories — create helper functions for generating test objects with sensible defaults
  8. Clean up — if a test modifies global state (localStorage, cookies), restore it in afterEach

Knowledge Strategy

  • Patterns to capture: Reusable MSW handler configurations, custom render wrappers for project-specific providers, test data factory patterns, recurring accessibility violation fixes.
  • Examples to collect: Successful test suites with high coverage, effective integration test patterns for complex pages, MSW handler compositions for multi-step API flows.
  • Update permission: Agents may freely add/update files in references/. Changes to SKILL.md or scripts/ require user approval.

What ships with it: 6 files

32.8 KB alongside SKILL.md

workflows/

Keep looking

Skills are one crate of 326,835. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.