Vitest Test Writer
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 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
/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
tests/unit/components/[component-name].test.tsx
tests/unit/hooks/[hook-name].test.ts
Required imports
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
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.
// ✅ 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
vi.mock('next/link', () => ({
default: ({ children, href, ...props }: any) => <a href={href} {...props}>{children}</a>,
}))
Supabase client mock
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:
// ❌ 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
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
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
beforeEach(() => {
mockPush.mockClear()
stableSearchParams.delete('search')
stableSearchParams.delete('tag')
stableSearchParams.delete('sort')
})
Async assertions
// Wait for async state updates
await waitFor(() => {
expect(screen.getByText('Success')).toBeInTheDocument()
})
What to Test
Components — cover these cases
- Renders without crashing (smoke test)
- Renders key content (title, description, labels)
- Link hrefs are correct
- Conditional rendering (show X when Y is true, hide when false)
- User interactions: click, type, submit
- Loading state while async operation is pending
- Error state when operation fails
- Empty state when data is absent
Hooks — cover these cases
- Initial return value
- State transitions after trigger
- Cleanup on unmount (no lingering timers/listeners)
- Edge cases (rapid toggling, zero delay)
Output Format
Complete test file, ready to save. Include:
- All necessary mocks at the top (before component import)
- A
makeX()factory function for complex test data describeblock withittests grouped logically- No skipped or placeholder tests — every test must be asserting something real
$20 more to next tier
Created by
Info
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>