CtrlK
BlogDocsLog inGet started
Tessl Logo

product-factory/testing

Apply this skill when writing, reviewing, or planning tests for a Next.js + TypeScript + Drizzle application using Vitest, React Testing Library, MSW, and Playwright. Triggers on requests like "write tests for", "add test coverage", "test this component", "test this hook", "test this API route", "add E2E tests", "is this testable", or any time you are building features that need verification. Use proactively — all new features should include tests.

87

Quality

87%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Skills
Evals
Files

SKILL.md

name:
testing
description:
Apply this skill when writing, reviewing, or planning tests for a Next.js + TypeScript + Drizzle application using Vitest, React Testing Library, MSW, and Playwright. Triggers on requests like "write tests for", "add test coverage", "test this component", "test this hook", "test this API route", "add E2E tests", "is this testable", or any time you are building features that need verification. Use proactively — all new features should include tests.

Testing: Next.js + TypeScript + Vitest + React Testing Library + Playwright

Testing Pyramid

LayerToolWhat to testVolume
UnitVitestPure functions, utils, schemas, transformsMany
IntegrationVitest + RTL + MSWHooks, components with data fetching, formsSome
E2EPlaywrightCritical user flows, auth, multi-page workflowsFew

Unit Tests

Pure Functions and Utils

// lib/__tests__/utils.test.ts
import { describe, it, expect } from 'vitest'
import { formatCurrency, slugify } from '../utils'

describe('formatCurrency', () => {
  it('formats USD with two decimals', () => {
    expect(formatCurrency(1234.5)).toBe('$1,234.50')
  })

  it('handles zero', () => {
    expect(formatCurrency(0)).toBe('$0.00')
  })
})

Zod Schemas

Test edge cases and error messages — these are your runtime safety net.

import { UserInsertSchema } from '../schema'

describe('UserInsertSchema', () => {
  it('rejects invalid email', () => {
    const result = UserInsertSchema.safeParse({ email: 'not-email', name: 'Test' })
    expect(result.success).toBe(false)
  })

  it('rejects empty name', () => {
    const result = UserInsertSchema.safeParse({ email: 'a@b.com', name: '' })
    expect(result.success).toBe(false)
  })

  it('accepts valid input', () => {
    const result = UserInsertSchema.safeParse({ email: 'a@b.com', name: 'Test' })
    expect(result.success).toBe(true)
  })
})

Drizzle Query Helpers

Test transform functions and query builders — not the database itself.

import { buildUserFilters } from '../queries'

describe('buildUserFilters', () => {
  it('filters by active status', () => {
    const filters = buildUserFilters({ active: true })
    expect(filters).toHaveLength(1)
  })
})

Component Tests

Setup — Providers Wrapper

// tests/test-utils.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, type RenderOptions } from '@testing-library/react'

export function renderWithProviders(ui: React.ReactElement, options?: RenderOptions) {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  })

  return render(
    <QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>,
    options,
  )
}

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

Component with User Interaction

import { render, screen } from '@/tests/test-utils'
import userEvent from '@testing-library/user-event'
import { UserForm } from '../UserForm'

describe('UserForm', () => {
  it('calls onSubmit with form values', async () => {
    const user = userEvent.setup()
    const onSubmit = vi.fn()

    render(<UserForm onSubmit={onSubmit} />)

    await user.type(screen.getByLabelText('Email'), 'test@example.com')
    await user.type(screen.getByLabelText('Name'), 'Test User')
    await user.click(screen.getByRole('button', { name: /save/i }))

    expect(onSubmit).toHaveBeenCalledWith({
      email: 'test@example.com',
      name: 'Test User',
    })
  })

  it('shows validation error for invalid email', async () => {
    const user = userEvent.setup()
    render(<UserForm onSubmit={vi.fn()} />)

    await user.type(screen.getByLabelText('Email'), 'not-email')
    await user.click(screen.getByRole('button', { name: /save/i }))

    expect(screen.getByText(/invalid email/i)).toBeInTheDocument()
  })
})

Query Best Practices for Testing

  • Query by role (getByRole) — tests what screen readers see
  • Query by label (getByLabelText) — tests that labels are wired
  • Query by text (getByText) — for static content
  • Avoid getByTestId unless there's no semantic alternative
  • Use findBy* (async) for elements that appear after data loads

Hook Tests

TanStack Query Hooks with MSW

import { renderHook, waitFor } from '@testing-library/react'
import { useUsers } from '../useUsers'
import { createWrapper } from '@/tests/test-utils'
import { server } from '@/tests/mocks/server'
import { http, HttpResponse } from 'msw'

describe('useUsers', () => {
  it('fetches users', async () => {
    server.use(
      http.get('/api/users', () => {
        return HttpResponse.json([
          { id: '1', name: 'Alice', email: 'alice@test.com' },
        ])
      }),
    )

    const { result } = renderHook(() => useUsers(), { wrapper: createWrapper() })

    await waitFor(() => expect(result.current.isSuccess).toBe(true))
    expect(result.current.data).toHaveLength(1)
    expect(result.current.data[0].name).toBe('Alice')
  })

  it('handles server error', async () => {
    server.use(
      http.get('/api/users', () => {
        return HttpResponse.json({ error: 'Server error' }, { status: 500 })
      }),
    )

    const { result } = renderHook(() => useUsers(), { wrapper: createWrapper() })

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

MSW Setup

// tests/mocks/handlers.ts
import { http, HttpResponse } from 'msw'

export const handlers = [
  http.get('/api/users', () => {
    return HttpResponse.json([])
  }),
  // Add default handlers for all endpoints used in tests
]

// tests/mocks/server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'

export const server = setupServer(...handlers)

// tests/setup.ts
import { server } from './mocks/server'

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

E2E Tests (Playwright)

Critical User Flows Only

E2E tests are slow — reserve them for flows that cross multiple pages or involve real auth.

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

test('user can sign up and reach dashboard', async ({ page }) => {
  await page.goto('/signup')
  await page.getByLabel('Email').fill('new@example.com')
  await page.getByLabel('Password').fill('SecurePass123!')
  await page.getByRole('button', { name: /sign up/i }).click()

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

Use data-testid Sparingly

Only for elements with no semantic role or text (e.g., a specific card in a Kanban column).

Page Object Pattern for Complex Flows

class PipelinePage {
  constructor(private page: Page) {}

  async goto() { await this.page.goto('/pipeline') }
  async getColumn(name: string) { return this.page.getByRole('region', { name }) }
  async getCandidateCard(name: string) { return this.page.getByText(name) }
}

Accessibility Testing

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

expect.extend(toHaveNoViolations)

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

What to Test — Priority Order

PriorityWhatWhy
P0Zod schemasRuntime safety net — wrong validation = wrong data in DB
P0Auth flows (E2E)Broken auth = security incident
P1Forms with validationUsers interact with these directly
P1Data fetching hooksIncorrect cache/query behaviour causes stale UI
P1Complex business logicPure functions with branching logic
P2Components with conditional renderingEdge cases in UI state
P2Error statesUsers should see meaningful errors, not blank screens
P3Static presentational componentsLow risk, low reward

Test File Conventions

  • Co-locate tests: src/features/users/__tests__/useUsers.test.ts
  • Or use a tests directory: tests/hooks/useUsers.test.ts
  • Name files: *.test.ts or *.test.tsx
  • E2E files: e2e/*.spec.ts
  • Factories/helpers: tests/helpers.ts or tests/factories.ts

Install with Tessl CLI

npx tessl i product-factory/testing@0.1.0

SKILL.md

tile.json