agentsclimarketplace

Test ts

Skill sergeyklay/.agents/.agents/skills/test-ts

My personal, curated set of artifacts for AI coding agents

Install
npx -y skills add sergeyklay/.agents --skill test-ts

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

One thing to look at

  • 5 stars5 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.

What its author says it does

Copied from the file, not written here

Write, review, and run TypeScript/React tests for this Next.js 16 App Router project. Use whenever writing or modifying *.test.ts or *.test.tsx files, adding test coverage to components, hooks, Server Actions, or utilities, setting up Vitest configuration, or asked about testing strategy. Covers Vitest (the project's test runner, not Jest), React Testing Library v16, accessibility-first queries, user-event for interactions, AAA structure, parameterized tests with describe.each and it.each, Prisma mock patterns, next/navigation and next/headers mocking, and the RSC boundary testing strategy. Do NOT use for Playwright end-to-end tests or performance benchmarks.

SKILL.md

15.1 KB, as published. Nobody here has run it

TypeScript/React testing

This project uses Vitest as the test runner. The Next.js 16 official documentation recommends Vitest over Jest for App Router projects: native ESM and TypeScript support require no additional transformation configuration, and Vitest runs 3–5x faster than Jest on equivalent suites. Do not introduce Jest.

Test runner commands

npm test                                   # run all tests once
npm run test:watch                         # watch mode
npm run test:coverage                      # with coverage report

# Targeted runs
npx vitest run src/components/invoice/     # run tests in a directory
npx vitest run -t "renders the client"     # filter by test name pattern
npx vitest run path/to/component.test.tsx  # run a single file

Decision framework

Before writing any test, classify it:

CategoryWhat it coversVitest environment
UnitPure functions, utilities, Zod schemas, computed logicnode
ComponentClient Components rendered with RTLjsdom
HookCustom hooks via renderHookjsdom
Server Action'use server' functions with mocked Prismanode
IntegrationReal database or external servicenode + env gate

jsdom is the default environment configured in vitest.config.ts. For Server Actions and utilities, add a file-level directive to switch to node:

// @vitest-environment node
import { describe, it, expect, vi, beforeEach } from 'vitest';

Pick the lightest category that validates the behavior. Async Server Components (RSCs) cannot be rendered by RTL; see "RSC testing strategy" below and references/rsc-patterns.md for full mocking recipes.

File organization

  • One test file per source file: invoice-card.tsx maps to invoice-card.test.tsx.
  • Co-locate test files next to the source file, not in a separate __tests__/ directory.
  • Fixture factories live in src/__fixtures__/<domain>.fixtures.ts.
  • All new features require tests; every bug fix requires a regression test.

Canonical test structure

Every test file follows the Arrange/Act/Assert pattern separated by blank lines. Do NOT write // Arrange, // Act, or // Assert comments. Tests read like a specification.

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi, beforeEach } from 'vitest';

import { InvoiceCard } from './invoice-card';
import { buildInvoice } from '@/__fixtures__/invoice.fixtures';

describe('InvoiceCard', () => {
  it('renders the client name and formatted amount', () => {
    const invoice = buildInvoice({ clientName: 'Acme Corp', amount: 1500 });

    render(<InvoiceCard invoice={invoice} />);

    expect(screen.getByRole('heading', { name: 'Acme Corp' })).toBeInTheDocument();
    expect(screen.getByText('$1,500.00')).toBeInTheDocument();
  });

  it('calls onMarkPaid with the invoice id when the mark-paid button is clicked', async () => {
    const user = userEvent.setup();
    const onMarkPaid = vi.fn();
    const invoice = buildInvoice({ status: 'unpaid' });

    render(<InvoiceCard invoice={invoice} onMarkPaid={onMarkPaid} />);
    await user.click(screen.getByRole('button', { name: /mark paid/i }));

    expect(onMarkPaid).toHaveBeenCalledExactlyOnceWith(invoice.id);
  });

  it('disables the mark-paid button when isPending is true', () => {
    const invoice = buildInvoice({ status: 'unpaid' });

    render(<InvoiceCard invoice={invoice} isPending />);

    expect(screen.getByRole('button', { name: /mark paid/i })).toBeDisabled();
  });
});

Structural rules:

  • describe names the component or function under test. Nest describe blocks to scope a scenario or method.
  • it reads as a complete sentence: it('disables the submit button while the mutation is pending').
  • Each it block covers one logical scenario. Split complex assertions into focused tests.

RTL query priority

Use queries in accessibility-first order:

  1. getByRole (first choice; exercises semantic HTML)
  2. getByLabelText (for labeled form inputs)
  3. getByPlaceholderText (fallback for unlabeled inputs)
  4. getByText (for non-interactive text content)
  5. getByTestId (last resort; add data-testid only when no semantic query fits)
// ✅ Accessibility-first
screen.getByRole('button', { name: /save invoice/i });
screen.getByLabelText('Invoice amount');
screen.getByRole('alert');
screen.getByRole('combobox', { name: 'Status' });

// ❌ Coupled to implementation details
screen.getByTestId('save-btn');
container.querySelector('.invoice-form');

User interactions

Always use @testing-library/user-event, not fireEvent. user-event simulates the full browser event sequence (pointerdown, focus, input, keydown, keyup, click). fireEvent dispatches a single synthetic event and misses intermediary behavior that real components react to.

const user = userEvent.setup();

await user.type(screen.getByLabelText('Client name'), 'Acme Corp');
await user.selectOptions(screen.getByRole('combobox', { name: 'Status' }), 'paid');
await user.click(screen.getByRole('button', { name: /save/i }));
await user.keyboard('{Escape}');
await user.clear(screen.getByRole('textbox', { name: 'Amount' }));

Parameterized tests

Use describe.each or it.each whenever multiple inputs share the same execution logic. Never loop with .forEach inside a single it block.

// ✅ Parameterized: each case gets its own entry in the test report
it.each([
  { status: 'paid',    label: 'Paid',    expectedClass: 'bg-green-100'  },
  { status: 'overdue', label: 'Overdue', expectedClass: 'bg-red-100'    },
  { status: 'pending', label: 'Pending', expectedClass: 'bg-yellow-100' },
  { status: 'draft',   label: 'Draft',   expectedClass: 'bg-muted'      },
])('renders "$label" badge with the correct color for $status status', ({ status, label, expectedClass }) => {
  render(<StatusBadge status={status} />);

  const badge = screen.getByRole('status');
  expect(badge).toHaveTextContent(label);
  expect(badge).toHaveClass(expectedClass);
});

// ✅ describe.each when each group needs multiple it blocks
describe.each([
  { dueDate: new Date('2025-01-01'), expectedStatus: 'overdue'  },
  { dueDate: new Date('2030-01-01'), expectedStatus: 'upcoming' },
])('invoice due $dueDate', ({ dueDate, expectedStatus }) => {
  it('displays the correct status badge', () => { ... });
  it('sorts before invoices due later', () => { ... });
});

// ❌ forEach hides failures and conflates scenarios
it('handles all badge statuses', () => {
  ['paid', 'overdue', 'pending'].forEach((status) => {
    render(<StatusBadge status={status} />);
    // ...
  });
});

Fixture factories

Build domain objects with factory functions, not inline literals. Inline literals couple tests to schema shape and break silently when fields are added or renamed.

// src/__fixtures__/invoice.fixtures.ts
import type { Invoice } from '@/types/invoice.types';

export function buildInvoice(overrides: Partial<Invoice> = {}): Invoice {
  return {
    id: 'inv_test_001',
    clientName: 'Test Client',
    amount: 1000,
    currency: 'USD',
    status: 'unpaid',
    dueDate: new Date('2026-12-31'),
    createdAt: new Date('2026-01-01'),
    userId: 'user_test_001',
    ...overrides,  // must be last so callers can override any field
  };
}

Co-locate factories with the domain type. One factory per domain entity. Name them build<Entity>.

Server Action testing

Call the Server Action function directly. Mock the Prisma client at the module boundary.

// @vitest-environment node
import { describe, it, expect, vi, beforeEach } from 'vitest';

import { markInvoicePaid } from '@/lib/actions/invoice-actions';

vi.mock('@/lib/db', () => ({
  db: {
    invoice: {
      findUnique: vi.fn(),
      update: vi.fn(),
    },
  },
}));

vi.mock('next/cache', () => ({
  revalidateTag: vi.fn(),
}));

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

  it('returns success and revalidates the invoices cache tag', async () => {
    const { db } = await import('@/lib/db');
    const { revalidateTag } = await import('next/cache');
    vi.mocked(db.invoice.findUnique).mockResolvedValue(buildInvoice({ userId: 'user_1' }));
    vi.mocked(db.invoice.update).mockResolvedValue(buildInvoice({ status: 'paid' }));

    const result = await markInvoicePaid({ invoiceId: 'inv_test_001', paidAt: new Date('2026-03-01') });

    expect(result).toEqual({ success: true });
    expect(revalidateTag).toHaveBeenCalledWith('invoices');
  });

  it('returns field errors without calling the database when invoiceId is empty', async () => {
    const { db } = await import('@/lib/db');

    const result = await markInvoicePaid({ invoiceId: '', paidAt: new Date() });

    expect(result.success).toBe(false);
    expect(result.fieldErrors?.invoiceId).toBeDefined();
    expect(db.invoice.update).not.toHaveBeenCalled();
  });
});

Rules:

  • vi.clearAllMocks() in beforeEach prevents state from leaking between tests.
  • vi.mocked() provides typed access to mock functions. Never cast to any.
  • Import mocked modules inside the test body with await import(...) after vi.mock() hoisting.
  • Always test the Zod validation-failure path. Do not only cover the happy path.

For Prisma chains with relations, pagination, or $transaction, see references/mocking-patterns.md.

RSC testing strategy

Async Server Components cannot be rendered by React Testing Library. They execute on the server and return RSC payloads; jsdom has no mechanism for this. Use three complementary approaches.

Extract and unit-test the data layer. If an RSC calls db.invoice.findMany(), extract that into a standalone async function and test it in the node environment with a mocked Prisma client. The RSC becomes a thin rendering shell.

// src/lib/data/invoice-data.ts (extracted, testable)
export async function getInvoicesForUser(userId: string): Promise<Invoice[]> {
  return db.invoice.findMany({ where: { userId }, orderBy: { createdAt: 'desc' } });
}
// @vitest-environment node
it('returns an empty array when the user has no invoices', async () => {
  vi.mocked(db.invoice.findMany).mockResolvedValue([]);

  const result = await getInvoicesForUser('user_123');

  expect(result).toEqual([]);
});

Test the Client Component leaves. The interactive parts of RSC trees are Client Components. Test those normally with RTL. The RSC serves as a server-rendered shell; the interactive leaves are fully covered by component tests.

Use E2E for full-page rendering. Playwright renders through the real Next.js server, the only environment where RSCs execute. Use Playwright for assertions that require the full RSC pipeline: Suspense boundaries, loading.tsx states, streaming.

// ❌ Async Server Components are server functions; render() does not work
render(<InvoicesPage />);

// ✅ Test the extracted data function in node environment
// ✅ Test the Client Component leaf separately
// ✅ Test the full page with Playwright

For mocking next/navigation, next/headers, cookies(), and Auth.js auth() in code called by RSCs or Server Actions, see references/rsc-patterns.md.

Hook testing

Test custom hooks with renderHook from React Testing Library. Wrap timer-dependent behavior with vi.useFakeTimers().

import { renderHook, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';

import { useJobPoller } from './use-job-poller';

describe('useJobPoller', () => {
  beforeEach(() => { vi.useFakeTimers(); });
  afterEach(() => { vi.useRealTimers(); });

  it('starts in pending state when a jobId is provided', () => {
    const { result } = renderHook(() => useJobPoller('job_001'));

    expect(result.current.status).toBe('pending');
  });

  it('transitions to completed when the polling interval resolves', async () => {
    const fetchStatus = vi.fn().mockResolvedValue('completed');

    const { result } = renderHook(() => useJobPoller('job_001', { fetchStatus }));
    await act(() => vi.runAllTimersAsync());

    expect(result.current.status).toBe('completed');
    expect(fetchStatus).toHaveBeenCalledWith('job_001');
  });

  it('returns null status when no jobId is provided', () => {
    const { result } = renderHook(() => useJobPoller(null));

    expect(result.current.status).toBeNull();
  });
});

Forbidden patterns

PatternReason
Snapshot tests for complex UIBrittle; asserts structure, not behavior. Allowed only for simple deterministic pure-output functions.
any in mock typesDefeats TypeScript strictness in test files. Use vi.mocked() for typed mocks.
Real database callsSlow, order-dependent, and fragile in CI. Mock Prisma at the module boundary.
fireEvent for user interactionsSkips the full browser event sequence. Use userEvent instead.
// Arrange, // Act, // Assert commentsTests should read naturally. Blank lines separate the phases.
Inline domain object literalsCouples tests to schema shape. Use fixture factories.
.forEach inside a single it blockHides individual failures in the report. Use it.each instead.
Multiple unrelated assertions per itMakes failure diagnosis harder. One scenario per test.
jest.* APIsThis project uses Vitest. The equivalent APIs are vi.fn(), vi.mock(), vi.mocked(), vi.spyOn().

Validation checklist

After writing or modifying tests, verify:

  • npm test passes with no failures
  • // @vitest-environment node present on all Server Action and utility test files
  • RTL queries use getByRole, getByLabelText, or getByText as the first choice
  • All user interactions use userEvent, not fireEvent
  • Multiple-input scenarios use it.each or describe.each, not .forEach
  • Domain objects built with fixture factories; no raw inline literals
  • vi.clearAllMocks() called in beforeEach when tests share mocked modules
  • vi.mocked() used for typed mock access; no as jest.Mock casts
  • Async Server Component logic extracted into standalone testable functions
  • Zod validation-failure paths covered in Server Action tests
  • New features have tests; bug fixes have regression tests

Keep looking

Skills are one crate of 328,083. 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.