Skip to content

Latest commit

 

History

History
1305 lines (992 loc) · 29.7 KB

File metadata and controls

1305 lines (992 loc) · 29.7 KB

Testing Guide for JudgeFinder.io

Comprehensive testing documentation for the JudgeFinder Platform.

Version: 1.1.0 Last Updated: 2025-11-09 Status: Production

Table of Contents

  1. Overview
  2. Testing Philosophy
  3. Test Structure
  4. Running Tests
  5. Writing Tests
  6. Unit Testing
  7. Integration Testing
  8. E2E Testing
  9. Test Patterns
  10. Mocking Guide
  11. Test Fixtures
  12. Test Coverage
  13. CI/CD Integration
  14. Best Practices
  15. Troubleshooting
  16. ID-Length Strategy

Overview

The JudgeFinder testing infrastructure consists of three main types of tests:

  1. Unit Tests: Test individual functions and business logic in isolation
  2. Integration Tests: Test API endpoints and database interactions
  3. E2E Tests: Test complete user flows through the browser

Technology Stack

  • Vitest: Fast unit and integration testing framework
  • Playwright: Browser automation for E2E testing
  • Testing Library: React component testing utilities
  • Happy DOM: Lightweight DOM implementation for unit tests

Testing Philosophy

Why We Test

Testing is not optional—it's a core part of development at JudgeFinder:

  • Confidence: Ship features without fear
  • Documentation: Tests serve as executable documentation
  • Regression Prevention: Catch bugs before users do
  • Refactoring Safety: Change code with confidence
  • Design Feedback: Tests reveal design issues early

Testing Pyramid

        /\
       /  \        E2E Tests (5%)
      /    \       - Critical user flows
     /------\      - Real browser automation
    /        \
   /          \    Integration Tests (25%)
  /            \   - API endpoint testing
 /              \  - Database integration
/----------------\
|                | Unit Tests (70%)
|                | - Pure functions
|                | - Business logic
|                | - Helper utilities
------------------

70% Unit Tests: Fast, isolated, abundant 25% Integration Tests: API routes, database, external services 5% E2E Tests: Critical user flows only

Coverage Targets

  • New Features: >80% coverage required
  • Bug Fixes: Add test that reproduces bug first
  • Critical Paths: >90% coverage (auth, billing, search)
  • Overall Project: Maintain >75% coverage

Test Structure

Directory Organization

tests/
├── setup/
│   └── test-setup.ts           # Global test configuration
├── fixtures/
│   ├── judges.ts               # Mock judge data
│   ├── cases.ts                # Mock case data
│   └── users.ts                # Mock user data
├── unit/
│   ├── auth/
│   │   ├── is-admin.test.ts
│   │   ├── bar-number-validation.test.ts
│   │   └── turnstile.test.ts
│   ├── analytics/
│   │   ├── bias-calculations.test.ts
│   │   └── report-generation.test.ts
│   ├── api/
│   │   ├── judges/
│   │   │   └── search-helpers.test.ts
│   │   └── stripe-webhook.test.ts
│   ├── lib/
│   │   ├── analytics/
│   │   └── utils/
│   └── validation/
│       ├── input-validation.test.ts
│       └── security-validation.test.ts
├── integration/
│   ├── api/
│   │   ├── search-route.test.ts
│   │   ├── chat-route.test.ts
│   │   └── judges-analytics.test.ts
│   └── stripe-flow.test.ts
├── e2e/
│   ├── auth/
│   │   └── sign-up-flow.spec.ts
│   ├── search/
│   │   └── judge-search.spec.ts
│   └── homepage-blur-animation.test.ts
├── migrations/
│   ├── analytics-cache.test.ts
│   ├── data-integrity.test.ts
│   └── security-definer-views.test.ts
└── helpers/
    ├── auth-helpers.ts
    ├── db-helpers.ts
    └── mock-helpers.ts

Naming Conventions

File Names:

  • {module-name}.test.ts for unit tests
  • {feature}-route.test.ts for API routes
  • {feature}-workflow.test.ts for E2E tests

Test Descriptions:

// ✅ Good - Descriptive, behavior-focused
describe('calculateBiasIndicators', () => {
  it('should calculate bias indicators within valid ranges', () => {
    // ...
  })

  it('should handle edge case with single case', () => {
    // ...
  })
})

// ❌ Bad - Vague, implementation-focused
describe('Bias', () => {
  it('works', () => {
    // ...
  })
})

AAA Pattern (Arrange, Act, Assert)

All tests should follow the AAA pattern:

it('should calculate settlement preference correctly', () => {
  // Arrange - Set up test data and conditions
  const cases: CaseRecord[] = Array(10)
    .fill(null)
    .map(() => ({ outcome: 'Settled' }))

  const caseTypePatterns = analyzeCaseTypePatterns(cases)
  const outcomeAnalysis = analyzeOutcomes(cases)

  // Act - Execute the code under test
  const indicators = calculateBiasIndicators(cases, caseTypePatterns, outcomeAnalysis)

  // Assert - Verify the results
  expect(indicators.settlement_preference).toBeGreaterThan(0)
})

Running Tests

All Tests

# Run all unit and integration tests
npm test

# Run all tests including E2E
npm run test:all

Unit Tests

# Run unit tests once
npm run test:unit

# Run in watch mode during development
npm run test:watch

# Run specific test file
npm run test:unit tests/unit/auth/is-admin.test.ts

Integration Tests

# Run all integration tests
npm run test:integration

# Run specific integration test
npm run test:integration tests/integration/api/search.test.ts

E2E Tests

# Run E2E tests (headless)
npm run test:e2e

# Run with UI mode for debugging
npm run test:e2e:ui

# Run in headed mode (see browser)
npm run test:e2e:headed

# Run specific browser
npm run test:e2e -- --project=chromium

Coverage

# Generate coverage report
npm run test:coverage

# Coverage report will be in coverage/index.html
open coverage/index.html  # macOS
start coverage/index.html # Windows

Writing Tests

Unit Tests

Unit tests should test individual functions in isolation with mocked dependencies.

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { myFunction } from '@/lib/my-module'

// Mock dependencies
vi.mock('@/lib/dependency', () => ({
  dependencyFunction: vi.fn(() => 'mocked value'),
}))

describe('MyFunction', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('should return expected result', () => {
    const result = myFunction('input')
    expect(result).toBe('expected output')
  })

  it('should handle edge cases', () => {
    expect(myFunction('')).toBe('')
    expect(myFunction(null)).toBe(null)
  })
})

Unit Testing

What to Unit Test

  • Pure functions
  • Business logic
  • Data transformations
  • Helper utilities
  • Validators
  • Calculations

Example 1: Testing Pure Functions

// tests/unit/lib/utils/slug.test.ts
import { describe, it, expect } from 'vitest'
import { generateJudgeSlug, parseJudgeSlug } from '@/lib/utils/slug'

describe('generateJudgeSlug', () => {
  it('should generate slug from judge name and ID', () => {
    const slug = generateJudgeSlug('John Smith', 123)
    expect(slug).toBe('john-smith-123')
  })

  it('should handle special characters', () => {
    const slug = generateJudgeSlug("O'Brien-Smith", 456)
    expect(slug).toBe('obrien-smith-456')
  })

  it('should handle empty name gracefully', () => {
    const slug = generateJudgeSlug('', 999)
    expect(slug).toBe('judge-999')
  })
})

Example 2: Testing Validators

// tests/unit/validation/input-validation.test.ts
import { describe, it, expect } from 'vitest'
import { validateSearchParams } from '@/lib/validation/search'

describe('validateSearchParams', () => {
  it('should validate correct search parameters', () => {
    const result = validateSearchParams({
      query: 'judge smith',
      limit: 20,
      page: 1,
    })

    expect(result.success).toBe(true)
    expect(result.data.query).toBe('judge smith')
  })

  it('should enforce limit maximum', () => {
    const result = validateSearchParams({
      query: 'test',
      limit: 1000,
    })

    expect(result.success).toBe(false)
    expect(result.error.message).toContain('Limit cannot exceed 500')
  })

  it('should sanitize SQL injection attempts', () => {
    const result = validateSearchParams({
      query: "'; DROP TABLE judges; --",
    })

    expect(result.success).toBe(true)
    expect(result.data.query).not.toContain('DROP TABLE')
  })
})

Example 3: Testing Calculations

// tests/unit/analytics/bias-calculations.test.ts
import { describe, it, expect } from 'vitest'
import { calculateBiasIndicators } from '@/lib/analytics/bias-calculations'

describe('calculateBiasIndicators', () => {
  it('should calculate bias indicators within valid ranges', () => {
    const cases: CaseRecord[] = [
      {
        case_type: 'Civil',
        outcome: 'Settled',
        case_value: 100000,
        filing_date: '2023-01-01',
        decision_date: '2023-03-01',
      },
      {
        case_type: 'Civil',
        outcome: 'Settled',
        case_value: 150000,
        filing_date: '2023-02-01',
        decision_date: '2023-04-01',
      },
    ]

    const patterns = analyzeCaseTypePatterns(cases)
    const analysis = analyzeOutcomes(cases)
    const indicators = calculateBiasIndicators(cases, patterns, analysis)

    // Verify all metrics are in valid ranges
    expect(indicators.consistency_score).toBeGreaterThanOrEqual(0)
    expect(indicators.consistency_score).toBeLessThanOrEqual(100)
    expect(indicators.settlement_preference).toBeGreaterThanOrEqual(-50)
    expect(indicators.settlement_preference).toBeLessThanOrEqual(50)
  })
})

Example 4: Testing Error Handling

// tests/unit/api/judges/search-helpers.test.ts
import { describe, it, expect } from 'vitest'
import { parseSearchParams } from '@/app/api/judges/search/helpers'

describe('parseSearchParams', () => {
  it('should handle malformed URLs gracefully', () => {
    const request = new Request('http://example.com?invalid=%%')
    expect(() => parseSearchParams(request)).not.toThrow()
  })

  it('should provide default values for missing parameters', () => {
    const request = new Request('http://example.com')
    const params = parseSearchParams(request)

    expect(params.query).toBe('')
    expect(params.limit).toBe(20)
    expect(params.page).toBe(1)
  })
})

Example 5: Testing Data Transformations

// tests/unit/lib/utils/helpers.test.ts
import { describe, it, expect } from 'vitest'
import { formatCurrency, formatDate } from '@/lib/utils/helpers'

describe('formatCurrency', () => {
  it('should format USD currency correctly', () => {
    expect(formatCurrency(1234.56)).toBe('$1,234.56')
    expect(formatCurrency(1000000)).toBe('$1,000,000.00')
  })

  it('should handle zero and negative values', () => {
    expect(formatCurrency(0)).toBe('$0.00')
    expect(formatCurrency(-500)).toBe('-$500.00')
  })

  it('should handle null/undefined', () => {
    expect(formatCurrency(null)).toBe('$0.00')
    expect(formatCurrency(undefined)).toBe('$0.00')
  })
})

Integration Testing

What to Integration Test

  • API endpoints
  • Database queries
  • External service integration
  • Authentication flows
  • Multi-step processes

Integration Tests

Integration tests verify API endpoints and database interactions work together correctly.

import { describe, it, expect, vi } from 'vitest'
import { NextRequest } from 'next/server'
import { GET } from '@/app/api/my-endpoint/route'

vi.mock('@/lib/supabase/server', () => ({
  createServerClient: vi.fn(async () => ({
    from: vi.fn(() => ({
      select: vi.fn(() => ({
        eq: vi.fn(() => ({
          data: mockData,
          error: null,
        })),
      })),
    })),
  })),
}))

describe('API /api/my-endpoint', () => {
  it('should return data successfully', async () => {
    const request = new NextRequest('http://localhost:3000/api/my-endpoint')
    const response = await GET(request)
    const data = await response.json()

    expect(response.status).toBe(200)
    expect(data).toBeDefined()
  })
})

Example 1: Basic API Route Test

// tests/integration/api/search-route.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { GET } from '@/app/api/judges/search/route'
import { mockAuthenticatedSession, createMockRequest } from '../../helpers/auth-helpers'

describe('GET /api/judges/search', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('should return search results for valid query', async () => {
    mockAuthenticatedSession()

    const request = createMockRequest('http://localhost:3000/api/judges/search?q=judge+smith')

    const response = await GET(request)
    const json = await response.json()

    expect(response.status).toBe(200)
    expect(json.results).toBeDefined()
    expect(Array.isArray(json.results)).toBe(true)
  })

  it('should support pagination', async () => {
    mockAuthenticatedSession()

    const request = createMockRequest('http://localhost:3000/api/judges/search?page=2&limit=10')

    const response = await GET(request)
    const json = await response.json()

    expect(response.status).toBe(200)
    expect(json.page).toBe(2)
    expect(json.per_page).toBe(10)
  })
})

Example 2: Testing Rate Limiting

describe('Rate Limiting', () => {
  it('should enforce 10 searches per day for anonymous users', async () => {
    mockUnauthenticatedSession()

    const mockRateLimiter = mockRateLimiterBlock(0)

    const request = createMockRequestWithIP(
      'http://localhost:3000/api/judges/search?q=judge',
      '192.168.1.1'
    )

    const response = await GET(request)
    const json = await response.json()

    expect(response.status).toBe(429)
    expect(json.error).toContain('Daily search limit reached')
  })

  it('should include rate limit headers', async () => {
    mockAuthenticatedSession()

    const request = createMockRequest('http://localhost:3000/api/judges/search?q=judge')
    const response = await GET(request)

    expect(response.headers.get('X-RateLimit-Limit')).toBeDefined()
    expect(response.headers.get('X-RateLimit-Remaining')).toBeDefined()
  })
})

Example 3: Testing Authentication

// tests/integration/api/auth-flow.test.ts
describe('Authentication Flow', () => {
  it('should reject unauthenticated requests', async () => {
    mockUnauthenticatedSession()

    const request = createMockRequest('http://localhost:3000/api/user/profile')
    const response = await GET(request)

    expect(response.status).toBe(401)
  })

  it('should allow authenticated requests', async () => {
    mockAuthenticatedSession('user-123')

    const request = createMockRequest('http://localhost:3000/api/user/profile')
    const response = await GET(request)

    expect(response.status).toBe(200)
  })
})

E2E Testing

What to E2E Test

E2E tests should cover critical user flows only:

  • User registration and login
  • Judge search and viewing
  • Subscription purchase
  • Profile management

E2E Tests

E2E tests verify complete user journeys through the browser.

import { test, expect } from '@playwright/test'

test.describe('User Flow', () => {
  test('should complete user journey', async ({ page }) => {
    await page.goto('/')

    // Interact with the page
    await page.getByRole('button', { name: 'Search' }).click()

    // Verify results
    await expect(page.getByText('Results')).toBeVisible()
  })
})

Example 1: Basic User Flow

// tests/e2e/judge-search-flow.test.ts
import { test, expect } from '@playwright/test'

test.describe('Judge Search Flow', () => {
  test('should search for judges and view profile', async ({ page }) => {
    await page.goto('/')

    // Enter search query
    await page.fill('input[placeholder="Search judges..."]', 'Smith')
    await page.press('input[placeholder="Search judges..."]', 'Enter')

    // Wait for results
    await expect(page.getByText(/results/i)).toBeVisible()

    // Click first result
    const results = page.locator('[data-testid="judge-card"]')
    await results.first().click()

    // Verify profile page loaded
    await expect(page.getByRole('heading', { level: 1 })).toBeVisible()
  })

  it('should handle empty search results', async ({ page }) => {
    await page.goto('/')

    await page.fill('input[placeholder="Search judges..."]', 'xyznonexistent')
    await page.press('input[placeholder="Search judges..."]', 'Enter')

    await expect(page.getByText(/no results found/i)).toBeVisible()
  })
})

Example 2: Authentication Flow

// tests/e2e/auth-flow.test.ts
test.describe('Authentication Flow', () => {
  test('should sign up new user', async ({ page }) => {
    await page.goto('/sign-up')

    await page.fill('input[name="email"]', 'test@example.com')
    await page.fill('input[name="password"]', 'SecurePassword123!')
    await page.click('button[type="submit"]')

    await expect(page).toHaveURL('/dashboard')
    await expect(page.getByText(/Welcome/i)).toBeVisible()
  })

  test('should handle invalid credentials', async ({ page }) => {
    await page.goto('/sign-in')

    await page.fill('input[name="email"]', 'wrong@example.com')
    await page.fill('input[name="password"]', 'WrongPassword')
    await page.click('button[type="submit"]')

    await expect(page.getByText(/invalid credentials/i)).toBeVisible()
  })
})

Test Patterns

Testing React Components

// tests/unit/components/JudgeCard.test.tsx
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { JudgeCard } from '@/components/judges/JudgeCard'

describe('JudgeCard', () => {
  const mockJudge = {
    id: 1,
    name: 'Judge Smith',
    court_name: 'Superior Court',
    total_cases: 1000,
    slug: 'judge-smith-1',
  }

  it('should render judge information', () => {
    render(<JudgeCard judge={mockJudge} />)

    expect(screen.getByText('Judge Smith')).toBeInTheDocument()
    expect(screen.getByText('Superior Court')).toBeInTheDocument()
    expect(screen.getByText('1,000 cases')).toBeInTheDocument()
  })

  it('should link to judge profile', () => {
    render(<JudgeCard judge={mockJudge} />)

    const link = screen.getByRole('link')
    expect(link).toHaveAttribute('href', '/judges/judge-smith-1')
  })
})

Testing Custom Hooks

// tests/unit/hooks/useJudgeSearch.test.ts
import { describe, it, expect } from 'vitest'
import { renderHook, waitFor } from '@testing-library/react'
import { useJudgeSearch } from '@/hooks/useJudgeSearch'

describe('useJudgeSearch', () => {
  it('should fetch search results', async () => {
    const { result } = renderHook(() => useJudgeSearch('smith'))

    expect(result.current.isLoading).toBe(true)

    await waitFor(() => {
      expect(result.current.isLoading).toBe(false)
    })

    expect(result.current.results).toBeDefined()
  })
})

Testing Async Code

// tests/unit/lib/analytics/report-builder.test.ts
describe('generateBiasReport', () => {
  it('should generate report asynchronously', async () => {
    const report = await generateBiasReport(123)

    expect(report).toBeDefined()
    expect(report.judgeId).toBe(123)
    expect(report.metrics).toBeDefined()
  })

  it('should handle errors gracefully', async () => {
    await expect(generateBiasReport(-1)).rejects.toThrow('Invalid judge ID')
  })
})

Mocking Guide

Mock External Dependencies

Always mock external services (databases, APIs, AI services):

vi.mock('@google/generative-ai', () => ({
  GoogleGenerativeAI: vi.fn(() => ({
    getGenerativeModel: vi.fn(() => ({
      generateContent: vi.fn(),
    })),
  })),
}))

Mocking Modules

// Mock entire module
vi.mock('@/lib/supabase/server', () => ({
  createServerClient: vi.fn().mockResolvedValue({
    from: vi.fn().mockReturnValue({
      select: vi.fn().mockResolvedValue({ data: [], error: null }),
    }),
  }),
}))

// Mock specific functions
vi.mock('@/lib/utils/helpers', () => ({
  formatCurrency: vi.fn((value) => `$${value}`),
  formatDate: vi.fn((date) => date.toISOString()),
}))

Mocking Fetch/API Calls

// Mock global fetch
global.fetch = vi.fn(() =>
  Promise.resolve({
    ok: true,
    status: 200,
    json: () => Promise.resolve({ data: 'mocked' }),
  })
) as any

// Verify fetch was called
expect(fetch).toHaveBeenCalledWith('/api/judges/search?q=smith')

Mocking Database

// Mock Supabase client
const mockSupabase = {
  from: vi.fn().mockReturnThis(),
  select: vi.fn().mockReturnThis(),
  eq: vi.fn().mockReturnThis(),
  single: vi.fn().mockResolvedValue({
    data: { id: 1, name: 'Judge Smith' },
    error: null,
  }),
}

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

Mock Functions

const mockFn = vi.fn()
mockFn.mockReturnValue('value')
mockFn.mockResolvedValue('async value')
mockFn.mockRejectedValue(new Error('error'))

Test Fixtures

Using Test Fixtures

Test fixtures provide consistent mock data across tests:

import { mockJudges, mockJudgesList } from '@/tests/fixtures/judges'
import { mockCases, generateMockCases } from '@/tests/fixtures/cases'
import { mockUsers } from '@/tests/fixtures/users'

// Use in tests
const testJudge = mockJudges.activeJudge
const multipleCases = generateMockCases(50, 'judge-001')

Creating Test Fixtures

// tests/fixtures/judges.ts
export const mockJudge = {
  id: 1,
  name: 'Judge John Smith',
  court_name: 'Superior Court of California, Los Angeles County',
  jurisdiction: 'CA',
  total_cases: 1234,
  slug: 'judge-john-smith-1',
}

export const mockJudgeList = [
  mockJudge,
  { ...mockJudge, id: 2, name: 'Judge Jane Doe' },
  { ...mockJudge, id: 3, name: 'Judge Bob Wilson' },
]

// tests/fixtures/cases.ts
export const mockCase = {
  id: 1,
  judge_id: 1,
  case_number: '23-CV-12345',
  case_type: 'Civil',
  outcome: 'Settled',
  filing_date: '2023-01-15',
  case_value: 150000,
}

Use Factories

// tests/helpers/factories.ts
export const createMockJudge = (overrides = {}) => ({
  id: 'judge-1',
  name: 'Test Judge',
  ...overrides,
})

Test Coverage

Coverage Requirements

The project enforces the following minimum coverage thresholds:

  • Lines: 60%
  • Functions: 60%
  • Branches: 50%
  • Statements: 60%

Viewing Coverage

# Generate and view coverage report
npm run test:coverage
open coverage/index.html  # macOS
start coverage/index.html # Windows

Coverage Exclusions

The following are excluded from coverage:

  • node_modules/
  • .next/
  • tests/
  • **/*.d.ts
  • **/*.config.*
  • scripts/
  • ios/

Coverage Thresholds (vitest.config.ts)

export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'lcov'],
      exclude: ['node_modules/', 'tests/', '*.config.ts', 'scripts/'],
      thresholds: {
        statements: 75,
        branches: 70,
        functions: 75,
        lines: 75,
      },
    },
  },
})

CI/CD Integration

Tests run automatically on GitHub Actions for:

  • Pull requests to main or develop
  • Pushes to main or develop

CI Test Pipeline

  1. Linting: Code style and quality checks
  2. Type Checking: TypeScript compilation
  3. Unit Tests: Fast isolated tests
  4. Integration Tests: API and database tests
  5. E2E Tests: Browser-based user flows (Chromium, Firefox, WebKit)
  6. Coverage: Generate and upload coverage reports

CI Commands

# Run all CI checks locally
npm run test:ci

CI/CD Configuration

# .github/workflows/test.yml
name: Tests

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Upload coverage
        uses: codecov/codecov-action@v3

Best Practices

1. Test Naming

Use descriptive test names that explain the behavior:

// ✅ Good
it('should return 404 when judge does not exist')

// ❌ Bad
it('test judge endpoint')

2. Arrange-Act-Assert Pattern

Structure tests clearly:

it('should calculate settlement rate correctly', () => {
  // Arrange
  const cases = [{ outcome: 'Settled' }, { outcome: 'Dismissed' }]

  // Act
  const result = analyzeOutcomes(cases)

  // Assert
  expect(result.overall_settlement_rate).toBe(0.5)
})

3. Test Edge Cases

Include tests for:

  • Empty inputs
  • Null/undefined values
  • Error conditions
  • Rate limits
  • Invalid data

4. Keep Tests Isolated

Each test should be independent:

beforeEach(() => {
  vi.clearAllMocks()
  // Reset any shared state
})

DO ✅

  • Write descriptive test names
  • Test behavior, not implementation
  • Use data-testid for stable selectors
  • Mock external dependencies
  • Test edge cases and errors
  • Keep tests independent
  • Use AAA pattern (Arrange, Act, Assert)

DON'T ❌

  • Test implementation details
  • Rely on CSS selectors
  • Share state between tests
  • Make tests dependent on each other
  • Skip accessibility tests
  • Ignore flaky tests
  • Write slow tests

Troubleshooting

Tests Fail Locally But Pass in CI

  • Ensure you're using the correct Node version (20.x)
  • Run npm ci instead of npm install
  • Check for environment-specific configurations

Playwright Browser Issues

# Reinstall browsers
npx playwright install --with-deps

# Clear Playwright cache
npx playwright clean

Vitest Watch Mode Not Working

# Try clearing Vitest cache
npx vitest --clearCache

Mock Not Working

Ensure mocks are defined before imports:

// Mock must come before import
vi.mock('@/lib/module')

import { functionToTest } from '@/lib/module'

Coverage Not Generated

# Install coverage provider
npm install --save-dev @vitest/coverage-v8

# Run with coverage
npm run test:coverage

Tests Timeout

// Increase timeout
test('slow test', async () => {
  // test code
}, 30000) // 30 second timeout

Flaky Tests

// Retry failed tests
test.retry(2)

Clear Mocks

beforeEach(() => {
  vi.clearAllMocks()
})

Debug Output

screen.debug() // Print DOM
console.log(screen.getByRole('button').textContent)

ID-Length Strategy

Production Code: Strict Rules

In production code (app/, components/, lib/), strict id-length rules apply:

// ✅ Good - Descriptive identifiers (min-length: 2)
const judgeId = 123
const searchQuery = 'smith'
const caseList = []

// ❌ Bad - Single letter identifiers
const j = 123 // Use judgeId
const q = 'smith' // Use query or searchQuery
const c = [] // Use cases or caseList

Exceptions in Production:

  • Loop iterators in tight scopes: for (let i = 0; i < 10; i++)
  • Array methods with obvious context: cases.map((c) => c.id)
  • Coordinates: { x, y } in graphics code

Test Files: Hybrid Approach

In test files (tests/**/*.test.ts), we use a hybrid approach:

// ✅ Allowed - Descriptive test data
const mockJudgeData = { id: 1, name: 'Smith' }
const testCases = [...]

// ✅ Allowed - Single-letter in obvious test context
describe('analyzeCaseTypePatterns', () => {
  it('should sort by total cases descending', () => {
    const c: CaseRecord[] = [
      { case_type: 'Family', outcome: 'Settled' },
      { case_type: 'Civil', outcome: 'Settled' },
    ]

    const p = analyzeCaseTypePatterns(c)

    expect(p[0].case_type).toBe('Civil')
  })
})

ESLint Configuration

// .eslintrc.js
module.exports = {
  rules: {
    'id-length': [
      'error',
      {
        min: 2,
        exceptions: ['i', 'j', 'k', 'x', 'y', 'z'],
        properties: 'never',
      },
    ],
  },
  overrides: [
    {
      // Relaxed rules for test files
      files: ['**/*.test.ts', '**/*.test.tsx', '**/tests/**/*'],
      rules: {
        'id-length': ['warn', { min: 1 }],
      },
    },
  ],
}

Additional Resources


Contributing Tests

When adding new features:

  1. Write unit tests for business logic
  2. Add integration tests for API endpoints
  3. Create E2E tests for user-facing features
  4. Ensure coverage meets thresholds
  5. Update this documentation if needed

Support

For testing questions or issues:

  1. Check this documentation
  2. Review existing test examples in tests/ directory
  3. Consult the troubleshooting section
  4. Review Test Strategy
  5. Open an issue on GitHub

Last Updated: 2025-11-09 Maintained By: JudgeFinder Development Team