Vitest

Vitest and React Testing Library

Vitest 102,989 reuses Vite 25,978 's transform pipeline, so JSX, TypeScript and your @/* alias work with no separate compiler step. Install vitest, @vitejs/plugin-react, jsdom 21,691 and the Testing Library 221,825 packages — but bump @types/node first. create-next-app@16.3.5 writes "@types/node": "^20" while Vitest 5 declares peerOptional @types/node@"^22.0.0 || >=24.0.0", so an install against the untouched template ends in npm 2,036 error ... Fix the upstream dependency conflict.

vitest.config.mtsShell
export default defineConfig({
  plugins: [react()],
  resolve: { tsconfigPaths: true },
  test: {
    environment: 'jsdom',
    setupFiles: ['./vitest.setup.ts'],
    include: ['__tests__/**/*.test.{ts,tsx}'],
  },
});

The Next.js 10,514 guide still lists vite-tsconfig-paths; under Vitest 5 that plugin prints Vite now supports tsconfig paths resolution natively via the resolve.tsconfigPaths option, so use the option instead. vitest.setup.ts holds two lines: import '@testing-library/jest-dom/vitest' and afterEach(cleanup), since Testing Library unmounts automatically only when globals are on.

Client Components need nothing else — 'use client' is a marker for the bundler. QuantityPicker keeps a quantity in useState, shows it in an <output aria-label="Quantity"> and prints Total: {formatPrice(qty * unitCents)} under buttons labeled Increase and Decrease.

__tests__/quantity-picker.test.tsxTSX
test('raising the quantity updates the total', async () => {
  const user = userEvent.setup();
  render(<QuantityPicker unitCents={1200} />);
  await user.click(screen.getByRole('button', { name: 'Increase' }));
  expect(screen.getByLabelText('Quantity')).toHaveTextContent('2');
  expect(screen.getByText(/Total:/)).toHaveTextContent('Total: $24.00');
});

Run against a first draft that formatted unitCents instead of qty * unitCents:

Output of 89
$ npx vitest run
 FAIL  __tests__/quantity-picker.test.tsx > raising the quantity updates the total
Error: expect(element).toHaveTextContent()
Expected element to have text content:
  Total: $24.00
Received:
  Total: $12.00

Multiplying by qty turns that into Test Files 1 passed (1). The assertion reads rendered text, not useState: the queries come from Testing React Applications, and so does the discipline.