# Testing Guide

> Comprehensive guide to writing and running tests in Izri

## 📋 Overview

This guide covers testing strategies, tools, and best practices for Izri. Learn how to write unit tests, integration tests, and end-to-end tests for both frontend and backend.

## 🎯 Testing Philosophy

### Our Approach

- ✅ **Test behavior, not implementation**
- ✅ **Write tests that provide confidence**
- ✅ **Test user interactions, not internals**
- ✅ **Keep tests simple and readable**
- ✅ **Fast feedback loops**

### Testing Pyramid

```
           /\
          /  \    E2E Tests (Few)
         /____\   - Full user flows
        /      \  - Critical paths
       /________\ 
      /          \ Integration Tests (Some)
     /____________\ - API + Database
    /              \ - Component + Data
   /________________\ 
  /                  \ Unit Tests (Many)
 /____________________\ - Pure functions
                        - Utilities
                        - Business logic
```

## 🛠️ Testing Stack

### Frontend

| Tool | Purpose |
|------|---------|
| **Vitest** | Test runner (fast, Vite-based) |
| **Testing Library** | React component testing |
| **MSW** | API mocking |
| **Playwright** | E2E testing |

### Backend

| Tool | Purpose |
|------|---------|
| **Vitest** | Test runner |
| **Supertest** | HTTP testing |
| **Testcontainers** | Database testing |

## 🚀 Running Tests

### Quick Start

```bash
# Run all tests
pnpm test

# Run tests in watch mode
pnpm test:watch

# Run tests for specific package
pnpm test --filter @izri/web
pnpm test --filter @izri/api

# Run with coverage
pnpm test:coverage
```

### Test Commands

```json
// package.json
{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest run --coverage"
  }
}
```

## 🧪 Unit Testing

### Testing Utilities

**Example**: Testing a utility function

```typescript
// packages/shared/src/utils/format.ts
export function formatDate(date: Date): string {
  return new Intl.DateTimeFormat('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
  }).format(date)
}

export function slugify(text: string): string {
  return text
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-|-$/g, '')
}
```

**Test file**: `packages/shared/src/utils/format.test.ts`

```typescript
import { describe, it, expect } from 'vitest'
import { formatDate, slugify } from './format'

describe('formatDate', () => {
  it('formats date in US format', () => {
    const date = new Date('2025-01-10')
    expect(formatDate(date)).toBe('January 10, 2025')
  })

  it('handles leap years', () => {
    const date = new Date('2024-02-29')
    expect(formatDate(date)).toBe('February 29, 2024')
  })
})

describe('slugify', () => {
  it('converts text to lowercase slug', () => {
    expect(slugify('Hello World')).toBe('hello-world')
  })

  it('removes special characters', () => {
    expect(slugify('Hello@World!')).toBe('hello-world')
  })

  it('handles multiple spaces', () => {
    expect(slugify('Hello   World')).toBe('hello-world')
  })

  it('trims leading and trailing dashes', () => {
    expect(slugify('-Hello World-')).toBe('hello-world')
  })
})
```

**Run tests**:

```bash
cd packages/shared
pnpm test
```

### Testing Validation Schemas

**Example**: Zod schema

```typescript
// packages/shared/src/schemas/project.ts
import { z } from 'zod'

export const createProjectSchema = z.object({
  name: z.string().min(1).max(255),
  repository: z.string().url(),
  branch: z.string().default('main'),
  language: z.enum(['typescript', 'javascript', 'python']),
})

export type CreateProjectInput = z.infer<typeof createProjectSchema>
```

**Test file**:

```typescript
// packages/shared/src/schemas/project.test.ts
import { describe, it, expect } from 'vitest'
import { createProjectSchema } from './project'

describe('createProjectSchema', () => {
  it('validates correct input', () => {
    const input = {
      name: 'My Project',
      repository: 'https://github.com/user/repo',
      branch: 'main',
      language: 'typescript',
    }
    
    expect(() => createProjectSchema.parse(input)).not.toThrow()
  })

  it('rejects empty name', () => {
    const input = {
      name: '',
      repository: 'https://github.com/user/repo',
      language: 'typescript',
    }
    
    expect(() => createProjectSchema.parse(input)).toThrow()
  })

  it('rejects invalid URL', () => {
    const input = {
      name: 'Project',
      repository: 'not-a-url',
      language: 'typescript',
    }
    
    expect(() => createProjectSchema.parse(input)).toThrow()
  })

  it('defaults branch to main', () => {
    const input = {
      name: 'Project',
      repository: 'https://github.com/user/repo',
      language: 'typescript',
    }
    
    const result = createProjectSchema.parse(input)
    expect(result.branch).toBe('main')
  })
})
```

## ⚛️ React Component Testing

### Setup

**Install dependencies**:

```bash
cd apps/web
pnpm add -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event
```

**Vitest config**: `apps/web/vitest.config.ts`

```typescript
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths'

export default defineConfig({
  plugins: [react(), tsconfigPaths()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './test/setup.ts',
  },
})
```

**Test setup**: `apps/web/test/setup.ts`

```typescript
import '@testing-library/jest-dom/vitest'
import { cleanup } from '@testing-library/react'
import { afterEach } from 'vitest'

// Cleanup after each test
afterEach(() => {
  cleanup()
})

// Mock environment variables
process.env.VITE_API_URL = 'http://localhost:4000'
```

### Testing Components

**Example component**: `apps/web/app/components/ui/button.tsx`

```typescript
import { type ButtonHTMLAttributes } from 'react'
import { cn } from '@/lib/utils'

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'danger'
  size?: 'sm' | 'md' | 'lg'
}

export function Button({ 
  variant = 'primary', 
  size = 'md',
  className,
  children,
  ...props 
}: ButtonProps) {
  return (
    <button
      className={cn(
        'rounded font-medium',
        variant === 'primary' && 'bg-blue-500 text-white',
        variant === 'secondary' && 'bg-gray-200 text-gray-900',
        variant === 'danger' && 'bg-red-500 text-white',
        size === 'sm' && 'px-2 py-1 text-sm',
        size === 'md' && 'px-4 py-2',
        size === 'lg' && 'px-6 py-3 text-lg',
        className
      )}
      {...props}
    >
      {children}
    </button>
  )
}
```

**Test file**: `apps/web/app/components/ui/button.test.tsx`

```typescript
import { describe, it, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Button } from './button'

describe('Button', () => {
  it('renders with text', () => {
    render(<Button>Click me</Button>)
    expect(screen.getByText('Click me')).toBeInTheDocument()
  })

  it('applies primary variant by default', () => {
    render(<Button>Primary</Button>)
    const button = screen.getByText('Primary')
    expect(button).toHaveClass('bg-blue-500')
  })

  it('applies secondary variant', () => {
    render(<Button variant="secondary">Secondary</Button>)
    const button = screen.getByText('Secondary')
    expect(button).toHaveClass('bg-gray-200')
  })

  it('applies danger variant', () => {
    render(<Button variant="danger">Danger</Button>)
    const button = screen.getByText('Danger')
    expect(button).toHaveClass('bg-red-500')
  })

  it('handles click events', async () => {
    const handleClick = vi.fn()
    const user = userEvent.setup()
    
    render(<Button onClick={handleClick}>Click</Button>)
    await user.click(screen.getByText('Click'))
    
    expect(handleClick).toHaveBeenCalledTimes(1)
  })

  it('can be disabled', () => {
    render(<Button disabled>Disabled</Button>)
    expect(screen.getByText('Disabled')).toBeDisabled()
  })
})
```

### Testing with tRPC

**Mock tRPC client**:

```typescript
// apps/web/test/utils.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { httpBatchLink } from '@trpc/client'
import { createTRPCReact } from '@trpc/react-query'
import { type ReactNode } from 'react'
import type { AppRouter } from '@izri/trpc'

export const trpc = createTRPCReact<AppRouter>()

export function createWrapper() {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: { retry: false },
      mutations: { retry: false },
    },
  })

  const trpcClient = trpc.createClient({
    links: [
      httpBatchLink({
        url: 'http://localhost:4000/trpc',
      }),
    ],
  })

  return function Wrapper({ children }: { children: ReactNode }) {
    return (
      <trpc.Provider client={trpcClient} queryClient={queryClient}>
        <QueryClientProvider client={queryClient}>
          {children}
        </QueryClientProvider>
      </trpc.Provider>
    )
  }
}
```

**Test component with tRPC**:

```typescript
// apps/web/app/components/projects/project-list.test.tsx
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import { setupServer } from 'msw/node'
import { http, HttpResponse } from 'msw'
import { ProjectList } from './project-list'
import { createWrapper } from '../../../test/utils'

// Mock API responses
const server = setupServer(
  http.post('http://localhost:4000/trpc/projects.list', () => {
    return HttpResponse.json({
      result: {
        data: [
          { id: '1', name: 'Project 1', repository: 'https://github.com/a/b' },
          { id: '2', name: 'Project 2', repository: 'https://github.com/c/d' },
        ],
      },
    })
  })
)

beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

describe('ProjectList', () => {
  it('renders loading state', () => {
    render(<ProjectList />, { wrapper: createWrapper() })
    expect(screen.getByText('Loading...')).toBeInTheDocument()
  })

  it('renders projects', async () => {
    render(<ProjectList />, { wrapper: createWrapper() })
    
    await waitFor(() => {
      expect(screen.getByText('Project 1')).toBeInTheDocument()
      expect(screen.getByText('Project 2')).toBeInTheDocument()
    })
  })

  it('handles error state', async () => {
    server.use(
      http.post('http://localhost:4000/trpc/projects.list', () => {
        return HttpResponse.json({ error: 'Failed' }, { status: 500 })
      })
    )

    render(<ProjectList />, { wrapper: createWrapper() })
    
    await waitFor(() => {
      expect(screen.getByText(/error/i)).toBeInTheDocument()
    })
  })
})
```

## 🔌 Backend Testing

### tRPC Procedure Testing

**Example procedure**:

```typescript
// packages/trpc/src/routers/projects.ts
import { router, protectedProcedure } from '../trpc'
import { z } from 'zod'
import { projects } from '@izri/database/schema'

export const projectRouter = router({
  list: protectedProcedure.query(async ({ ctx }) => {
    return ctx.db
      .select()
      .from(projects)
      .where(eq(projects.userId, ctx.userId))
  }),

  create: protectedProcedure
    .input(z.object({
      name: z.string(),
      repository: z.string().url(),
    }))
    .mutation(async ({ ctx, input }) => {
      const [project] = await ctx.db
        .insert(projects)
        .values({
          ...input,
          userId: ctx.userId,
        })
        .returning()
      
      return project
    }),
})
```

**Test file**:

```typescript
// packages/trpc/src/routers/projects.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { appRouter } from '../router'
import { createTestContext } from '../../test/utils'

describe('projectRouter', () => {
  let ctx: Awaited<ReturnType<typeof createTestContext>>

  beforeAll(async () => {
    ctx = await createTestContext()
  })

  afterAll(async () => {
    await ctx.cleanup()
  })

  describe('list', () => {
    it('returns user projects', async () => {
      const caller = appRouter.createCaller(ctx)
      
      // Create test projects
      await caller.projects.create({
        name: 'Project 1',
        repository: 'https://github.com/a/b',
      })
      await caller.projects.create({
        name: 'Project 2',
        repository: 'https://github.com/c/d',
      })

      // Query projects
      const projects = await caller.projects.list()
      
      expect(projects).toHaveLength(2)
      expect(projects[0].name).toBe('Project 1')
      expect(projects[1].name).toBe('Project 2')
    })

    it('only returns current user projects', async () => {
      const caller = appRouter.createCaller(ctx)
      
      // Create project for current user
      await caller.projects.create({
        name: 'My Project',
        repository: 'https://github.com/a/b',
      })

      // Create context for different user
      const otherCtx = await createTestContext({ userId: 'other-user' })
      const otherCaller = appRouter.createCaller(otherCtx)
      
      const projects = await otherCaller.projects.list()
      
      expect(projects).toHaveLength(0)
      
      await otherCtx.cleanup()
    })
  })

  describe('create', () => {
    it('creates a project', async () => {
      const caller = appRouter.createCaller(ctx)
      
      const project = await caller.projects.create({
        name: 'New Project',
        repository: 'https://github.com/user/repo',
      })

      expect(project).toMatchObject({
        name: 'New Project',
        repository: 'https://github.com/user/repo',
        userId: ctx.userId,
      })
      expect(project.id).toBeDefined()
    })

    it('validates input', async () => {
      const caller = appRouter.createCaller(ctx)
      
      await expect(
        caller.projects.create({
          name: '',
          repository: 'not-a-url',
        })
      ).rejects.toThrow()
    })
  })
})
```

### Test Context Setup

```typescript
// packages/trpc/test/utils.ts
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import * as schema from '@izri/database/schema'

export async function createTestContext(overrides?: Partial<Context>) {
  // Use test database
  const connection = postgres(process.env.TEST_DATABASE_URL!)
  const db = drizzle(connection, { schema })

  const userId = overrides?.userId || 'test-user-123'

  const context = {
    db,
    redis: null, // Mock Redis if needed
    userId,
    ...overrides,
  }

  const cleanup = async () => {
    // Clean up test data
    await db.delete(schema.projects).where(eq(schema.projects.userId, userId))
    await connection.end()
  }

  return { ...context, cleanup }
}
```

### Database Integration Tests

**Using Testcontainers**:

```typescript
// packages/database/test/integration.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { PostgreSqlContainer } from '@testcontainers/postgresql'
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import { migrate } from 'drizzle-orm/postgres-js/migrator'
import * as schema from '../src/schema'

describe('Database Integration', () => {
  let container: PostgreSqlContainer
  let db: ReturnType<typeof drizzle>

  beforeAll(async () => {
    // Start PostgreSQL container
    container = await new PostgreSqlContainer().start()
    
    const connection = postgres(container.getConnectionUri())
    db = drizzle(connection, { schema })
    
    // Run migrations
    await migrate(db, { migrationsFolder: './migrations' })
  }, 60000) // Increase timeout for container startup

  afterAll(async () => {
    await container.stop()
  })

  it('creates and retrieves projects', async () => {
    const [project] = await db
      .insert(schema.projects)
      .values({
        name: 'Test Project',
        repository: 'https://github.com/test/repo',
        userId: 'user-123',
      })
      .returning()

    expect(project.name).toBe('Test Project')

    const retrieved = await db
      .select()
      .from(schema.projects)
      .where(eq(schema.projects.id, project.id))

    expect(retrieved[0]).toMatchObject(project)
  })
})
```

## 🌐 E2E Testing with Playwright

### Setup

**Install Playwright**:

```bash
cd apps/web
pnpm add -D @playwright/test
pnpm exec playwright install
```

**Config**: `apps/web/playwright.config.ts`

```typescript
import { defineConfig, devices } from '@playwright/test'

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'http://localhost:5173',
    trace: 'on-first-retry',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],
  webServer: {
    command: 'pnpm dev',
    url: 'http://localhost:5173',
    reuseExistingServer: !process.env.CI,
  },
})
```

### Writing E2E Tests

```typescript
// apps/web/e2e/projects.spec.ts
import { test, expect } from '@playwright/test'

test.describe('Projects', () => {
  test('user can create a project', async ({ page }) => {
    // Navigate to projects page
    await page.goto('/projects')

    // Click "New Project" button
    await page.click('text=New Project')

    // Fill form
    await page.fill('input[name="name"]', 'My Test Project')
    await page.fill('input[name="repository"]', 'https://github.com/test/repo')
    await page.selectOption('select[name="language"]', 'typescript')

    // Submit
    await page.click('button[type="submit"]')

    // Verify redirect to project page
    await expect(page).toHaveURL(/\/projects\/[a-z0-9-]+/)

    // Verify project details
    await expect(page.locator('h1')).toContainText('My Test Project')
  })

  test('user can view project list', async ({ page }) => {
    await page.goto('/projects')

    // Wait for projects to load
    await page.waitForSelector('[data-testid="project-card"]')

    // Verify at least one project exists
    const projects = page.locator('[data-testid="project-card"]')
    await expect(projects).not.toHaveCount(0)
  })

  test('user can delete a project', async ({ page }) => {
    await page.goto('/projects')

    // Click first project
    await page.click('[data-testid="project-card"]:first-child')

    // Click delete button
    await page.click('text=Delete Project')

    // Confirm deletion
    await page.click('text=Confirm')

    // Verify redirect to projects list
    await expect(page).toHaveURL('/projects')

    // Verify toast message
    await expect(page.locator('[role="alert"]')).toContainText('Project deleted')
  })
})
```

**Run E2E tests**:

```bash
# Run all E2E tests
pnpm exec playwright test

# Run in UI mode
pnpm exec playwright test --ui

# Run specific test
pnpm exec playwright test projects.spec.ts

# Debug mode
pnpm exec playwright test --debug
```

## 📊 Test Coverage

### Generating Coverage

```bash
# Run tests with coverage
pnpm test:coverage

# View coverage report
open coverage/index.html
```

### Coverage Goals

- **Unit tests**: > 80% coverage
- **Integration tests**: Critical paths covered
- **E2E tests**: Happy paths covered

### Vitest Coverage Config

```typescript
// vitest.config.ts
export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: [
        'node_modules/',
        'test/',
        '**/*.test.ts',
        '**/*.spec.ts',
      ],
    },
  },
})
```

## ✅ Best Practices

### Do's

- ✅ **Test user behavior**, not implementation
- ✅ **Use descriptive test names**: `it('creates project with valid input')`
- ✅ **Arrange-Act-Assert** pattern
- ✅ **One assertion per test** (when possible)
- ✅ **Clean up after tests** (database, mocks)
- ✅ **Use test data builders** for complex objects
- ✅ **Mock external dependencies**

### Don'ts

- ❌ **Don't test implementation details**
- ❌ **Don't write flaky tests**
- ❌ **Don't test third-party libraries**
- ❌ **Don't share state between tests**
- ❌ **Don't skip tests** (fix or remove them)

### Test Structure

```typescript
describe('Feature', () => {
  // Setup
  beforeEach(() => {
    // Arrange
  })

  it('does something when condition', () => {
    // Arrange
    const input = createTestData()

    // Act
    const result = functionUnderTest(input)

    // Assert
    expect(result).toBe(expected)
  })

  // Cleanup
  afterEach(() => {
    cleanup()
  })
})
```

## 🔗 Related Documentation

- [Debugging](./debugging.md) - Debugging techniques
- [Running Services](./running-services.md) - Service management
- [Local Setup](./local-setup.md) - IDE configuration

---

**Need help with testing?** Ask in team chat or check examples in the codebase!
