active

Vitest Test Writer

Safe
System VerifiedSafe

Generate complete Vitest + Testing Library test files for Next.js components and hooks following SkillSlap's exact patterns: vi.mock for next/navigation and Supabase, getAllByText for dual-rendered elements, proper beforeEach cleanup.

@system/vitest-test-writer

vitest
testing
react
typescript
nextjs
skillslap

Vitest Test Writer

Purpose: Generate a complete, passing Vitest + Testing Library test file for a Next.js component or hook. The output must follow SkillSlap's exact test patterns — no guessing at mock shapes, no test utils the project doesn't have.


Invocation

code
/test <component-path> [description]

Examples:

  • /test components/skills/skill-card.tsx
  • /test components/skills/skill-search.tsx Tests all sort/filter/tag interactions
  • /test hooks/use-hover-intent.ts

SkillSlap Test Conventions

Test file location

code
tests/unit/components/[component-name].test.tsx
tests/unit/hooks/[hook-name].test.ts

Required imports

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

next/navigation mock — always needed for components using router/params

tsx
const mockPush = vi.fn()
const mockSearchParams = new URLSearchParams()

vi.mock('next/navigation', () => ({
  useRouter: () => ({ push: mockPush }),
  useSearchParams: () => mockSearchParams,
  usePathname: () => '/',
}))

Critical: useSearchParams() must return a stable reference across renders. Create it once outside vi.mock() and use that reference. Otherwise useEffect([searchParams]) will loop infinitely in tests.

tsx
// ✅ Stable reference
const stableSearchParams = new URLSearchParams()
vi.mock('next/navigation', () => ({
  useSearchParams: () => stableSearchParams,
  ...
}))

// ❌ New object every render — causes infinite loop
vi.mock('next/navigation', () => ({
  useSearchParams: () => new URLSearchParams(),
  ...
}))

next/link mock

tsx
vi.mock('next/link', () => ({
  default: ({ children, href, ...props }: any) => <a href={href} {...props}>{children}</a>,
}))

Supabase client mock

tsx
const mockFrom = vi.fn()
const mockSupabase = { from: mockFrom }

vi.mock('@/lib/supabase/client', () => ({
  createClient: () => mockSupabase,
}))

// In test: chain the query mock
beforeEach(() => {
  mockFrom.mockReturnValue({
    select: vi.fn().mockReturnThis(),
    eq: vi.fn().mockReturnThis(),
    order: vi.fn().mockReturnThis(),
    limit: vi.fn().mockResolvedValue({ data: [], error: null }),
    insert: vi.fn().mockResolvedValue({ data: null, error: null }),
  })
})

Dual-rendered elements (mobile + desktop)

Many SkillSlap components render the same content in both a mobile overlay and a desktop section. Use getAllByText not getByText:

tsx
// ❌ Fails if element appears twice
expect(screen.getByText('testuser')).toBeInTheDocument()

// ✅ Works for both mobile and desktop renders
expect(screen.getAllByText('testuser').length).toBeGreaterThan(0)
// Or target a specific instance:
const badge = screen.getAllByText('85%')[0].closest('[class]')

Timer mocking for debounce

tsx
beforeEach(() => {
  vi.useFakeTimers()
})
afterEach(() => {
  vi.useRealTimers()
})

// Advance timers in test
fireEvent.change(input, { target: { value: 'hello' } })
vi.advanceTimersByTime(400)  // match debounce delay
expect(mockPush).toHaveBeenCalledWith('/?search=hello')

Hooks testing

tsx
import { renderHook, act } from '@testing-library/react'

it('sets isHovering true after delay', async () => {
  vi.useFakeTimers()
  const { result } = renderHook(() => useHoverIntent({ delay: 100 }))
  act(() => { result.current.onMouseEnter() })
  expect(result.current.isHovering).toBe(false)  // not yet
  act(() => { vi.advanceTimersByTime(100) })
  expect(result.current.isHovering).toBe(true)
})

beforeEach cleanup pattern

tsx
beforeEach(() => {
  mockPush.mockClear()
  stableSearchParams.delete('search')
  stableSearchParams.delete('tag')
  stableSearchParams.delete('sort')
})

Async assertions

tsx
// Wait for async state updates
await waitFor(() => {
  expect(screen.getByText('Success')).toBeInTheDocument()
})

What to Test

Components — cover these cases

  1. Renders without crashing (smoke test)
  2. Renders key content (title, description, labels)
  3. Link hrefs are correct
  4. Conditional rendering (show X when Y is true, hide when false)
  5. User interactions: click, type, submit
  6. Loading state while async operation is pending
  7. Error state when operation fails
  8. Empty state when data is absent

Hooks — cover these cases

  1. Initial return value
  2. State transitions after trigger
  3. Cleanup on unmount (no lingering timers/listeners)
  4. Edge cases (rapid toggling, zero delay)

Output Format

Complete test file, ready to save. Include:

  1. All necessary mocks at the top (before component import)
  2. A makeX() factory function for complex test data
  3. describe block with it tests grouped logically
  4. No skipped or placeholder tests — every test must be asserting something real
Dormant$0/mo

$20 more to next tier

Info

Created February 24, 2026
Version 1.0.0
Agent-invoked
Terminal output

Embed

Add this skill card to any webpage.

<iframe src="https://skillslap.com/skill/931b5ec8-9f3a-4e04-bfa2-4f2ad7e556e4/embed"
        width="400" height="200"
        style="border:none;border-radius:12px;"
        title="SkillSlap Skill: Vitest Test Writer">
</iframe>