Appearance
Testing Strategy
DoCurious uses Vitest for unit/integration tests, React Testing Library for component rendering, Playwright for end-to-end tests, and Storybook for visual testing. A GitHub Actions CI pipeline runs the full suite on every push and PR.
STATUS: BUILT
~100+ frontend test files covering stores, adapters, pages, UI components, hooks, and API layer. Backend has 25 test files covering all 20 API modules.
Test Suite Overview
| Layer | Location | Count | What's Tested |
|---|---|---|---|
| Zustand stores | src/store/__tests__/ | ~25 | State logic, actions, selectors |
| Adapters | src/adapters/__tests__/ | ~6 | SQL→FE type mapping, status maps, ID promotion |
| Pages | src/pages/__tests__/ | ~20 | Page rendering, content display |
| UI primitives | src/components/ui/__tests__/ | ~15 | Props, accessibility, variants |
| Domain components | src/components/*/__tests__/ | ~15 | Gamification, filters, layout, challenges |
| Hooks | src/hooks/__tests__/ | ~3 | Feature flags, location |
| API layer | src/api/__tests__/ | ~2 | HTTP client, real API integration |
Tools
| Tool | Purpose |
|---|---|
| Vitest 4.0.18 | Test runner — provides describe/it/expect globals |
| jsdom 28.0.0 | DOM simulation for rendering React components in Node.js |
| @testing-library/react 16.3.2 | render(), screen, fireEvent, waitFor |
| @testing-library/jest-dom 6.9.1 | DOM matchers: toBeInTheDocument(), toHaveTextContent() |
| @testing-library/user-event 14.6.1 | Simulates real user interactions (click, type, tab) |
| Playwright | End-to-end browser tests (Chromium) |
| V8 coverage | Built-in code coverage via Vitest |
| Storybook 10.2.8 + a11y addon | Visual testing + accessibility audits |
Running Tests
bash
# Unit/integration — watch mode (re-runs on file changes)
npm test
# Unit/integration — single run (CI mode)
npm run test:run
# With coverage report (outputs to coverage/)
npm run test:coverage
# E2E — headless Chromium
npm run test:e2e
# E2E — with Playwright interactive UI
npm run test:e2e:ui
# E2E — headed browser (visible)
npm run test:e2e:headed
# Everything — Vitest + Playwright back-to-back
npm run test:allVitest Configuration
typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
globals: true, // No need to import describe/it/expect
environment: 'jsdom', // DOM simulation
setupFiles: './src/test/setup.ts',
css: true, // Process CSS imports
exclude: ['server/**', 'node_modules/**', 'e2e/**'],
coverage: {
provider: 'v8',
reporter: ['text', 'text-summary', 'lcov', 'html'],
reportsDirectory: './coverage',
include: ['src/**/*.{ts,tsx}'],
exclude: [
'src/**/*.stories.tsx',
'src/**/*.test.{ts,tsx}',
'src/test/**',
'src/types/**',
'src/api/mockDb.ts',
'src/api/seed.ts',
'src/components/debug/**',
'src/i18n/**',
'src/vite-env.d.ts',
],
thresholds: {
statements: 17,
branches: 16.5,
functions: 13.5,
lines: 17.5,
},
},
},
})Coverage Thresholds
The build fails if coverage drops below these minimums:
| Metric | Minimum |
|---|---|
| Statements | 17% |
| Branches | 16.5% |
| Functions | 13.5% |
| Lines | 17.5% |
These thresholds should be ratcheted up as coverage grows.
Test Setup
The setup file at src/test/setup.ts runs before every test file. It:
- Imports
@testing-library/jest-domfor DOM matchers - Mocks
ResizeObserver(not available in jsdom) - Mocks
matchMedia(not available in jsdom) - Mocks
IntersectionObserver(not available in jsdom) - Mocks
window.scrollTo(not available in jsdom)
CI Pipeline
The GitHub Actions workflow at .github/workflows/test.yml runs on every push and PR to main:
push/PR → fe-unit ─────────→ fe-coverage (uploads HTML report artifact)
→ be-integration ──┐
(Postgres 17) └→ e2e (Playwright + Chromium, uploads failure report)Jobs
| Job | Trigger | What it does |
|---|---|---|
| fe-unit | Every push/PR | Runs npm run test:run — all Vitest tests |
| fe-coverage | After fe-unit passes | Runs npm run test:coverage, uploads HTML report as artifact (14-day retention) |
| be-integration | Every push/PR | Spins up a Postgres 17 service container, runs server/ tests with test DB |
| e2e | After fe-unit + be-integration pass | Installs Chromium, runs Playwright, uploads failure report as artifact |
Concurrency
The workflow uses concurrency with cancel-in-progress: true, so pushing again while tests are running cancels the old run to save CI minutes.
When Tests Run
| Event | What runs |
|---|---|
Push to any branch targeting main | All 4 jobs |
Pull request to main | All 4 jobs |
| Local development | npm test (watch mode) |
How to Write a Test
Component Test
typescript
// src/components/challenge/__tests__/ChallengeCard.test.tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { ChallengeCard } from '../ChallengeCard'
const mockChallenge = {
id: '123',
title: 'Build a Birdhouse',
difficulty: 'beginner' as const,
isFree: true,
coverImageUrl: '/img/birdhouse.jpg',
completionCount: 42,
trackRecordCount: 15,
}
describe('ChallengeCard', () => {
it('renders the challenge title', () => {
render(<ChallengeCard challenge={mockChallenge} />)
expect(screen.getByText('Build a Birdhouse')).toBeInTheDocument()
})
it('shows the free badge when challenge is free', () => {
render(<ChallengeCard challenge={mockChallenge} />)
expect(screen.getByText('Free')).toBeInTheDocument()
})
it('calls onClick when card is clicked', async () => {
const user = userEvent.setup()
const onClick = vi.fn()
render(<ChallengeCard challenge={mockChallenge} onClick={onClick} />)
await user.click(screen.getByText('Build a Birdhouse'))
expect(onClick).toHaveBeenCalledWith('123')
})
})Store Test
typescript
// src/store/__tests__/useChallengeStore.test.ts
import { useChallengeStore } from '../useChallengeStore'
describe('useChallengeStore', () => {
beforeEach(() => {
useChallengeStore.setState({
challenges: [],
myChallenges: [],
isLoading: false,
})
})
it('starts with empty challenges', () => {
const state = useChallengeStore.getState()
expect(state.challenges).toEqual([])
})
it('sets loading state during fetch', async () => {
const promise = useChallengeStore.getState().fetchChallenges()
expect(useChallengeStore.getState().isLoading).toBe(true)
await promise
expect(useChallengeStore.getState().isLoading).toBe(false)
})
})Hook Test
typescript
// src/hooks/__tests__/useFeatureFlag.test.ts
import { renderHook } from '@testing-library/react'
import { useFeatureFlag } from '../useFeatureFlag'
describe('useFeatureFlag', () => {
it('returns disabled when no user is logged in', () => {
const { result } = renderHook(() => useFeatureFlag('wallet_v2'))
expect(result.current.enabled).toBe(false)
})
})API Test
typescript
// src/api/__tests__/challenge.api.test.ts
import { challengeApi } from '../'
describe('challengeApi', () => {
it('returns paginated challenges', async () => {
const response = await challengeApi.getChallenges()
expect(response.success).toBe(true)
expect(Array.isArray(response.data)).toBe(true)
expect(response.pagination).toBeDefined()
})
})Test File Conventions
| Convention | Pattern |
|---|---|
| Test file location | src/**/__tests__/*.test.ts(x) |
| Test file naming | ComponentName.test.tsx for components, storeName.test.ts for stores |
| Describe blocks | Component or module name |
| Test names | Start with a verb: "renders...", "calls...", "shows...", "returns..." |
Vitest Globals
With globals: true in the config, these are available without imports:
typescript
describe('group', () => { ... })
it('test', () => { ... })
expect(value).toBe(expected)
vi.fn() // Create mock function
vi.spyOn() // Spy on method
vi.mock() // Mock module
beforeEach() // Run before each test
afterEach() // Run after each test
beforeAll() // Run once before all tests
afterAll() // Run once after all testsStorybook
Storybook provides visual testing and component documentation with 55 story files:
bash
# Launch development server
npm run storybook # http://localhost:6006
# Build static site
npm run build-storybookAccessibility Testing
The @storybook/addon-a11y addon provides automatic accessibility audits for every story in the Storybook UI.
See Also
- Backend Testing -- backend test suite (25 files, Vitest + Supertest)
- Contributing -- development workflow and PR process
- Architecture -- frontend system layers