Skip to content
cccskills
FreePublished on GitHub

nextjs-testing

Testing strategies for Next.js: Server Components, Client Components, Server Actions, Route Handlers, end-to-end with Playwright. Vitest/Jest configuration, React Testing Library patterns, msw for network mocks. Use this skill to: - Pick the right test layer for what you're testing. - Configure Vitest or Jest for Next.js. - Test Server Components without running them in isolation. - Test Server Actions and Route Handlers as pure functions. - Set up Playwright for e2e and integration of RSC. Do NOT use this skill for: - General Next.js conventions (see nextjs-conventions). - RSC vs Client model (see server-component-patterns). - Plain Node.js test patterns (see nodejs-plugin equivalents).

The English summary is being prepared. Showing the original description.

Works withPlaywright
See how to install

Files included (1)

  • SKILL.md9.4 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Next.js Testing Patterns

Next.js testing splits along the same line as the runtime: server-side code runs in Node, client-side in jsdom or a browser. The right tool depends on the layer.

Test framework selection

LayerFramework
Client Components, hooks, plain JS/TS unitsVitest (preferred for new projects) or Jest
Server ComponentsPlaywright integration tests OR Vitest with the right config
Server ActionsVitest (treat the action body as a pure function — extract logic)
Route HandlersVitest with mocked NextRequest
End-to-end (full stack)Playwright (Vercel-recommended) or Cypress

For new projects, prefer Vitest + Playwright. Match what exists otherwise.

Vitest setup for Next.js

vitest.config.ts:

import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    setupFiles: ['./vitest.setup.ts'],
    globals: true,
    css: true,
  },
  resolve: {
    alias: { '@': path.resolve(__dirname, './') },
  },
});

vitest.setup.ts:

import '@testing-library/jest-dom/vitest';

Install: pnpm add -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event.

Testing Client Components (RTL)

// app/users/_components/UserFilter.test.tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserFilter } from './UserFilter';

describe('UserFilter', () => {
  it('calls onChange as user types', async () => {
    const onChange = vi.fn();
    const user = userEvent.setup();
    render(<UserFilter onChange={onChange} />);

    await user.type(screen.getByRole('textbox'), 'alice');

    expect(onChange).toHaveBeenLastCalledWith('alice');
  });
});

Query priority: getByRole > getByLabelText > getByText > getByTestId (last resort). Roles match how assistive tech sees the UI.

Testing Server Components

Server Components are async and run on the server — they don't render in jsdom directly. Two approaches:

Approach A — Extract data logic, test that

// app/users/page.tsx
import { db } from '@/lib/db';
import { UserList } from './_components/UserList';

export async function getUsers() {
  return db.users.findMany();
}

export default async function UsersPage() {
  const users = await getUsers();
  return <UserList users={users} />;
}

// app/users/page.test.ts
import { describe, it, expect, vi } from 'vitest';
import { getUsers } from './page';
import { db } from '@/lib/db';

vi.mock('@/lib/db', () => ({
  db: { users: { findMany: vi.fn().mockResolvedValue([{ id: '1', name: 'Alice' }]) } },
}));

describe('getUsers', () => {
  it('returns users from DB', async () => {
    expect(await getUsers()).toEqual([{ id: '1', name: 'Alice' }]);
  });
});

The view layer (UserList) is a pure component — test separately as a unit.

Approach B — Playwright integration

// e2e/users.spec.ts
import { test, expect } from '@playwright/test';

test('users page lists users', async ({ page }) => {
  await page.goto('/users');
  await expect(page.getByRole('listitem')).toHaveCount(3);
  await expect(page.getByText('Alice')).toBeVisible();
});

Run against a test database with known seed data, OR mock the API layer the page consumes.

Testing Server Actions

Server Actions are async functions — testable as pure code if you extract the logic:

// app/users/actions.ts
'use server';
import { z } from 'zod';
import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';

const Schema = z.object({ email: z.string().email(), name: z.string().min(1) });

export async function createUser(formData: FormData) {
  const parsed = Schema.safeParse(Object.fromEntries(formData));
  if (!parsed.success) return { ok: false as const, error: parsed.error.flatten() };
  const user = await db.users.create({ data: parsed.data });
  revalidatePath('/users');
  return { ok: true as const, user };
}
// app/users/actions.test.ts
import { describe, it, expect, vi } from 'vitest';
import { createUser } from './actions';

vi.mock('@/lib/db', () => ({
  db: { users: { create: vi.fn().mockResolvedValue({ id: '1', email: 'a@b.c', name: 'A' }) } },
}));
vi.mock('next/cache', () => ({ revalidatePath: vi.fn() }));

describe('createUser', () => {
  it('returns ok with valid input', async () => {
    const fd = new FormData();
    fd.set('email', 'a@b.c');
    fd.set('name', 'Alice');
    const result = await createUser(fd);
    expect(result.ok).toBe(true);
  });

  it('returns error on invalid input', async () => {
    const fd = new FormData();
    fd.set('email', 'invalid');
    const result = await createUser(fd);
    expect(result.ok).toBe(false);
  });
});

For the auth check at the top of the action, mock auth() similarly.

Testing Route Handlers

// app/api/users/route.test.ts
import { describe, it, expect, vi } from 'vitest';
import { NextRequest } from 'next/server';
import { GET, POST } from './route';

vi.mock('@/lib/db', () => ({
  db: { users: { findMany: vi.fn().mockResolvedValue([]), create: vi.fn() } },
}));

describe('GET /api/users', () => {
  it('returns 200 with users', async () => {
    const res = await GET();
    expect(res.status).toBe(200);
    expect(await res.json()).toEqual([]);
  });
});

describe('POST /api/users', () => {
  it('returns 400 on invalid body', async () => {
    const req = new NextRequest('http://localhost/api/users', {
      method: 'POST',
      body: JSON.stringify({ email: 'bad' }),
    });
    const res = await POST(req);
    expect(res.status).toBe(400);
  });
});

For dynamic routes:

import { GET } from './[id]/route';
const res = await GET(new NextRequest('...'), { params: Promise.resolve({ id: '123' }) });

(In Next.js 15+ params is Promise; otherwise plain object.)

Network mocking with msw

For component tests that hit fetch:

// vitest.setup.ts
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';

const server = setupServer(
  http.get('https://api.example.com/users', () =>
    HttpResponse.json([{ id: '1', name: 'Alice' }])
  )
);

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

Per-test override:

import { http, HttpResponse } from 'msw';
server.use(http.get('...', () => HttpResponse.error()));

Playwright setup

playwright.config.ts:

import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
  },
  projects: [
    { name: 'chromium', use: { browserName: 'chromium' } },
    { name: 'firefox', use: { browserName: 'firefox' } },
    { name: 'webkit', use: { browserName: 'webkit' } },
  ],
});

Install: pnpm dlx playwright install.

Coverage discipline

Target ≥80% on services, route handlers, server actions, and Client Components with logic. Skip:

  • Pure presentational components (snapshot churn outweighs value).
  • Layouts (mostly composition).
  • next.config.js, middleware.ts config bodies (test the behavior end-to-end).

Configure exclusions:

// vitest.config.ts
test: {
  coverage: {
    exclude: [
      'app/**/layout.tsx',
      'app/**/loading.tsx',
      'app/**/error.tsx',
      'app/**/not-found.tsx',
      '**/*.config.{js,ts,mjs}',
      'middleware.ts',
    ],
  },
}

Iteration cap (from QA agent)

The qa-engineer agent has a hard 3-attempt cap on fixing failing tests. If a test is fundamentally fragile after 3 attempts, mark it it.skip(...) with a comment explaining why and report in the QA summary. Don't iterate past the cap.

Anti-patterns

  • ❌ Trying to render a Server Component in jsdom directly. Extract data logic, test that; integration-test the rendered HTML via Playwright.
  • ❌ Asserting on implementation details (CSS classes, internal state). Assert on user-visible behavior.
  • ❌ getByTestId as the first choice. Use getByRole / getByLabelText first.
  • ❌ Real network calls in unit tests. Use msw or vi.mock at the module boundary.
  • ❌ Snapshot tests of large component trees. They turn into review noise.
  • ❌ E2E tests against localhost:3000 without webServer config — Playwright won't start the dev server.
  • ❌ Mocking the SUT instead of its dependencies.

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Angular 18-21 project structure, standalone components vs NgModule, control flow (@if/@for/@switch + *ngIf/*ngFor legacy), decorators, dependency injection (inject() function), lifecycle hooks, pipes, Angular Universal SSR pointer. Use this skill to: - Detect project style (standalone vs NgModule) and apply matching patterns. - Pick correct decorators and DI approach. - Use modern control flow (@if/@for/@switch) in Angular 17+ projects. - Apply `inject()` function over constructor injection where appropriate. - Wire bootstrap correctly (bootstrapApplication for standalone, AppModule for legacy). Do NOT use this skill for: - State management (see angular-state-and-rx). - Routing (see angular-routing). - Forms (see angular-forms). - Testing (see angular-testing).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc47Updated Oct 17, 2026

Angular forms: Reactive Forms (preferred — typed FormGroup/FormControl since Angular 14, FormBuilder, custom + async validators, FormArray, multi-step) and Template-driven (`[(ngModel)]` + FormsModule). Validation strategies, server error mapping, accessibility. Use this skill to: - Build Reactive Forms with typed FormGroup/FormControl. - Use FormBuilder для concise syntax. - Implement custom synchronous and async validators. - Wire FormArray for dynamic field lists. - Map server errors back to form fields. - Pick Reactive vs Template-driven (prefer Reactive). Do NOT use this skill for: - General conventions (see angular-conventions). - State management beyond forms (see angular-state-and-rx). - Routing (see angular-routing). - Testing forms (see angular-testing).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc47Updated Oct 17, 2026

Angular Router (built-in `@angular/router`) — route configuration for standalone and NgModule projects, functional guards (Angular 14.1+), lazy loading, route resolvers, typed params via signals/observables, programmatic navigation, route data and meta. Use this skill to: - Configure routes (standalone-style or NgModule-style). - Use functional guards (canActivate as function, preferred over class-based in 17+). - Lazy-load components or feature modules. - Implement auth guards via route meta + functional guards. - Read params/queries via `inject(ActivatedRoute)` + signals or RxJS. Do NOT use this skill for: - General conventions (see angular-conventions). - State management (see angular-state-and-rx). - Forms (see angular-forms). - Testing routes (see angular-testing).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc47Updated Oct 17, 2026

State management for Angular 18-21: signals (signal/computed/effect), services-as-state, NgRx Store + Effects + Selectors, NgRx Component Store, NgRx Signals (newer signal-based store). RxJS essentials — operators, async pipe, takeUntilDestroyed, signal/observable interop. Use this skill to: - Pick the right state tool (signals / services / NgRx variant / vue-query equivalent). - Use signals correctly (signal/computed/effect — when each). - Build a Pinia-style service-as-state singleton. - Set up NgRx Store + Effects + Selectors. - Use RxJS without leaking subscriptions (async pipe, takeUntilDestroyed, Subject patterns). - Bridge signals ↔ observables via toSignal / toObservable. Do NOT use this skill for: - General Angular conventions (see angular-conventions). - Routing state (see angular-routing). - Form state (see angular-forms). - Testing state (see angular-testing).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc47Updated Oct 17, 2026

Testing Angular 18-21: TestBed, component harnesses (@angular/cdk/testing), Karma+Jasmine (default historical) vs Jest (jest-preset-angular, modern), Angular Testing Library (RTL-style). HttpClient mocking via HttpTestingController. NgRx Effects testing. Cypress / Playwright e2e. Use this skill to: - Detect runner (Karma+Jasmine vs Jest) and configure correctly. - Write component tests with TestBed. - Use component harnesses for Material / custom UI components. - Mock HttpClient via provideHttpClientTesting + HttpTestingController. - Test signal-based inputs with componentRef.setInput(). - Test NgRx Effects with provideMockActions. Do NOT use this skill for: - General Angular conventions (see angular-conventions). - Routing patterns broadly (see angular-routing — covers testing routes briefly). - Form patterns broadly (see angular-forms).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc47Updated Oct 17, 2026

Shared conventions for every SDLC development-phase architect agent: hard rules, code quality bar, workflow steps (superpowers invocation, spec reading, codebase exploration, verification), and the report/compact-summary contract. Architects load this skill first, then apply their stack-specific instructions on top.

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc47Updated Oct 17, 2026

See all skills by AratKruglik

Report a problem with this skill