Development

Testing

How to write and run tests with Vitest and React Testing Library.

Testing Approach

Haze Dashboard uses Vitest for unit and component testing, combined with React Testing Library for rendering and interacting with components. Vitest is Vite-native, so it shares the same config and plugin pipeline as your dev server — tests run fast and resolve path aliases automatically.

Tests live in a dedicated tests/ directory or alongside the code they test. Use the .test.ts or .test.tsx file extension.

Setup

Install the testing dependencies:

npm install -D vitest @testing-library/react @testing-library/jest-dom happy-dom @vitejs/plugin-react

Create a vitest.config.ts at the project root:

// vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import { resolve } from 'path'

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'happy-dom',
    globals: true,
    include: ['tests/**/*.test.ts?(x)', 'src/**/*.test.ts?(x)'],
  },
  resolve: {
    alias: { '@': resolve(__dirname, 'src') },
  },
})

Writing a Component Test

Test that a component renders correctly with given props and responds to user interaction:

// tests/components/status-badge.test.tsx
import { describe, it, expect } from 'vitest'
import { render } from '@testing-library/react'
import { StatusBadge } from '@/components/shared/status-badge'

describe('StatusBadge', () => {
  it('renders the status text', () => {
    const { getByText } = render(<StatusBadge status="active" />)
    expect(getByText(/active/i)).toBeDefined()
  })

  it('applies success color for completed status', () => {
    const { container } = render(<StatusBadge status="completed" />)
    expect(container.innerHTML).toMatch(/green|success/)
  })

  it('applies error color for cancelled status', () => {
    const { container } = render(<StatusBadge status="cancelled" />)
    expect(container.innerHTML).toMatch(/red|error/)
  })
})

Writing a Page Test

Page tests verify that a page component renders its main elements:

// tests/pages/dashboard.test.tsx
import { describe, it, expect } from 'vitest'
import { render } from '@testing-library/react'
import DashboardPage from '@/app/(dashboard)/dashboard/page'

describe('Dashboard Page', () => {
  it('renders the page heading', () => {
    const { container } = render(<DashboardPage />)
    expect(container.innerHTML).toBeTruthy()
  })
})

Testing Hooks

Test custom hooks with React Testing Library's renderHook and act:

// tests/hooks/use-theme-settings.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { renderHook, act } from '@testing-library/react'
import { useThemeSettings } from '@/hooks/use-theme-settings'

describe('useThemeSettings', () => {
  beforeEach(() => {
    localStorage.clear()
  })

  it('starts with default settings', () => {
    const { result } = renderHook(() => useThemeSettings())
    expect(result.current.accentColor).toBe('teal')
    expect(result.current.density).toBe('default')
  })

  it('persists accent color changes', () => {
    const { result } = renderHook(() => useThemeSettings())
    act(() => result.current.setAccentColor('blue'))
    expect(result.current.accentColor).toBe('blue')
  })
})

Running Tests

CommandDescription
npx vitestRun tests in watch mode (re-runs on file changes)
npx vitest runRun all tests once and exit (CI-friendly)
npx vitest --uiOpen the interactive Vitest UI in a browser
npx vitest --coverageGenerate coverage report (requires @vitest/coverage-v8)
npx vitest run tests/components/Run tests in a specific directory

Mocking API Routes

Components that use fetch() can be tested by mocking the global fetch:

// tests/components/orders-list.test.tsx
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, waitFor } from '@testing-library/react'
import OrdersList from '@/components/orders-list'

beforeEach(() => {
  vi.stubGlobal('fetch', vi.fn(() =>
    Promise.resolve({
      json: () => Promise.resolve({
        data: [
          { id: 1, orderNumber: 'ORD-001', status: 'active' },
          { id: 2, orderNumber: 'ORD-002', status: 'pending' },
        ],
        meta: { total: 2, page: 1, perPage: 10, lastPage: 1 },
      }),
    } as Response)
  ))
})

describe('OrdersList', () => {
  it('renders orders from the API', async () => {
    const { getByText } = render(<OrdersList />)
    await waitFor(() => expect(getByText('ORD-001')).toBeDefined())
  })
})

Tip

For pure functions (utilities, formatters, permission helpers), write plain Vitest tests without any rendering setup. They're the fastest tests you can write and the easiest to maintain.

Next Steps

Ready to go live? See the Deployment guide for instructions on deploying to Vercel, Netlify, Cloudflare Pages, or a Node.js server.