Skip to main content
The Starter Kit includes comprehensive testing infrastructure using Vitest for integration tests and Playwright for end-to-end testing.

Test types

Integration tests (Vitest):
  • Test utilities and helper functions
  • Validate configuration logic
  • Check data transformations
  • Run fast without external dependencies
E2E tests (Playwright):
  • Test complete user workflows
  • Verify authentication flows
  • Check page navigation
  • Test form submissions

Running tests

All tests

Run both integration and E2E tests:
npm run test

Integration tests only

npm run test:integration
Watch mode for development:
npm run test:integration:watch

E2E tests only

npm run test:e2e
Run with UI mode:
npx playwright test --ui

Writing integration tests

Create tests in tests/integration/ or colocate with your code:
__tests__/utils.test.ts
import { describe, it, expect } from 'vitest'
import { sanitizeReturnUrl } from '@/lib/return-url'

describe('sanitizeReturnUrl', () => {
  it('allows valid relative paths', () => {
    expect(sanitizeReturnUrl('/dashboard')).toBe('/dashboard')
    expect(sanitizeReturnUrl('/profile')).toBe('/profile')
  })
  
  it('rejects absolute URLs', () => {
    expect(sanitizeReturnUrl('https://evil.com')).toBe(null)
  })
  
  it('rejects protocol-relative URLs', () => {
    expect(sanitizeReturnUrl('//evil.com/path')).toBe(null)
  })
})

Testing Server Actions

Test server-side logic:
__tests__/actions.test.ts
import { describe, it, expect, vi } from 'vitest'

// Mock fetch for testing
global.fetch = vi.fn()

describe('Server Actions', () => {
  it('handles API errors gracefully', async () => {
    vi.mocked(fetch).mockResolvedValueOnce({
      ok: false,
      status: 401,
      json: async () => ({ detail: 'Unauthorized' })
    } as Response)
    
    const result = await myServerAction()
    expect(result.error).toBe('Unauthorized')
  })
})

Writing E2E tests

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

test.describe('Authentication', () => {
  test('user can log in', async ({ page }) => {
    await page.goto('/login')
    
    // Fill login form
    await page.fill('[name="email"]', '[email protected]')
    await page.fill('[name="password"]', 'password123')
    
    // Submit form
    await page.click('button[type="submit"]')
    
    // Verify redirect to dashboard
    await expect(page).toHaveURL('/dashboard')
    
    // Verify user is logged in
    await expect(page.locator('text=Welcome')).toBeVisible()
  })
  
  test('shows error for invalid credentials', async ({ page }) => {
    await page.goto('/login')
    
    await page.fill('[name="email"]', '[email protected]')
    await page.fill('[name="password"]', 'wrongpassword')
    await page.click('button[type="submit"]')
    
    // Verify error message
    await expect(page.locator('text=Invalid credentials')).toBeVisible()
  })
})

Testing protected pages

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

test('redirects to login when not authenticated', async ({ page }) => {
  await page.goto('/dashboard')
  
  // Should redirect to login
  await expect(page).toHaveURL(/\/login/)
})

test('allows access when authenticated', async ({ page }) => {
  // Login first
  await page.goto('/login')
  await page.fill('[name="email"]', '[email protected]')
  await page.fill('[name="password"]', 'password123')
  await page.click('button[type="submit"]')
  
  // Navigate to protected page
  await page.goto('/dashboard')
  
  // Should allow access
  await expect(page).toHaveURL('/dashboard')
})

Test configuration

Vitest config

vitest.config.ts includes:
  • TypeScript support
  • Path aliases (@/ imports)
  • Coverage reporting
  • Fast watch mode

Playwright config

playwright.config.ts includes:
  • Multiple browsers (Chromium, Firefox, WebKit)
  • Mobile viewport testing
  • Screenshot on failure
  • Video recording for CI

CI/CD integration

Run tests in your CI pipeline:
.github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '19'
      
      - run: npm install
      - run: npm run test
      
      - uses: actions/upload-artifact@v3
        if: failure()
        with:
          name: playwright-report
          path: playwright-report/

Best practices

  • Test one thing per test case
  • Use descriptive test names
  • Group related tests with describe()
import { vi } from 'vitest'

// Mock Cloud API calls
global.fetch = vi.fn().mockResolvedValue({
  ok: true,
  json: async () => ({ id: '123', name: 'Test' })
})
Don’t just test happy paths:
  • Invalid inputs
  • Network failures
  • Permission errors
  • Edge cases
Create reusable test data:
const mockUser = {
  id: '123',
  email: '[email protected]',
  role: 'end_user'
}

Troubleshooting

Common issues:
  • Port conflicts: Ensure dev server isn’t running during E2E tests
  • Flaky tests: Add proper wait conditions with Playwright
  • Environment variables: Set test-specific values in test setup

Next steps