---
description: 
globs: 
alwaysApply: false
---
# Testing Patterns & Best Practices

## Running Tests

### Test Commands
From the **root directory** (formbricks/):
- `npm test` - Run all tests across all packages (recommended for CI/full testing)
- `npm run test:coverage` - Run all tests with coverage reports
- `npm run test:e2e` - Run end-to-end tests with Playwright

From the **apps/web directory** (apps/web/):
- `npm run test` - Run only web app tests (fastest for development)
- `npm run test:coverage` - Run web app tests with coverage
- `npm run test -- <file-pattern>` - Run specific test files

### Examples
```bash
# Run all tests from root (takes ~3 minutes, runs 790 test files with 5334+ tests)
npm test

# Run specific test file from apps/web (fastest for development)
npm run test -- modules/cache/lib/service.test.ts

# Run tests matching pattern from apps/web  
npm run test -- modules/ee/license-check/lib/license.test.ts

# Run with coverage from root
npm run test:coverage

# Run specific test with watch mode from apps/web (for development)
npm run test -- --watch modules/cache/lib/service.test.ts

# Run tests for a specific directory from apps/web
npm run test -- modules/cache/
```

### Performance Tips
- **For development**: Use `apps/web` directory commands to run only web app tests
- **For CI/validation**: Use root directory commands to run all packages
- **For specific features**: Use file patterns to target specific test files
- **For debugging**: Use `--watch` mode for continuous testing during development

### Test File Organization
- Place test files in the **same directory** as the source file
- Use `.test.ts` for utility/service tests (Node environment)
- Use `.test.tsx` for React component tests (jsdom environment)

## Test File Naming & Environment

### File Extensions
- Use `.test.tsx` for React component/hook tests (runs in jsdom environment)
- Use `.test.ts` for utility/service tests (runs in Node environment)
- The vitest config uses `environmentMatchGlobs` to automatically set jsdom for `.tsx` files

### Test Structure
```typescript
// Import the mocked functions first
import { useHook } from "@/path/to/hook";
import { serviceFunction } from "@/path/to/service";
import { renderHook, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, test, vi } from "vitest";

// Mock dependencies
vi.mock("@/path/to/hook", () => ({
  useHook: vi.fn(),
}));

describe("ComponentName", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    // Setup default mocks
  });

  test("descriptive test name", async () => {
    // Test implementation
  });
});
```

## React Hook Testing

### Context Mocking
When testing hooks that use React Context:
```typescript
vi.mocked(useResponseFilter).mockReturnValue({
  selectedFilter: {
    filter: [],
    onlyComplete: false,
  },
  setSelectedFilter: vi.fn(),
  selectedOptions: {
    questionOptions: [],
    questionFilterOptions: [],
  },
  setSelectedOptions: vi.fn(),
  dateRange: { from: new Date(), to: new Date() },
  setDateRange: vi.fn(),
  resetState: vi.fn(),
});
```

### Testing Async Hooks
- Always use `waitFor` for async operations
- Test both loading and completed states
- Verify API calls with correct parameters

```typescript
test("fetches data on mount", async () => {
  const { result } = renderHook(() => useHook());
  
  expect(result.current.isLoading).toBe(true);
  
  await waitFor(() => {
    expect(result.current.isLoading).toBe(false);
  });
  
  expect(result.current.data).toBe(expectedData);
  expect(vi.mocked(apiCall)).toHaveBeenCalledWith(expectedParams);
});
```

### Testing Hook Dependencies
To test useEffect dependencies, ensure mocks return different values:
```typescript
// First render
mockGetFormattedFilters.mockReturnValue(mockFilters);

// Change dependency and trigger re-render
const newMockFilters = { ...mockFilters, finished: true };
mockGetFormattedFilters.mockReturnValue(newMockFilters);
rerender();
```

## Performance Testing

### Race Condition Testing
Test AbortController implementation:
```typescript
test("cancels previous request when new request is made", async () => {
  let resolveFirst: (value: any) => void;
  let resolveSecond: (value: any) => void;

  const firstPromise = new Promise((resolve) => {
    resolveFirst = resolve;
  });
  const secondPromise = new Promise((resolve) => {
    resolveSecond = resolve;
  });

  vi.mocked(apiCall)
    .mockReturnValueOnce(firstPromise as any)
    .mockReturnValueOnce(secondPromise as any);

  const { result } = renderHook(() => useHook());
  
  // Trigger second request
  result.current.refetch();
  
  // Resolve in order - first should be cancelled
  resolveFirst!({ data: 100 });
  resolveSecond!({ data: 200 });

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

  // Should have result from second request
  expect(result.current.data).toBe(200);
});
```

### Cleanup Testing
```typescript
test("cleans up on unmount", () => {
  const abortSpy = vi.spyOn(AbortController.prototype, "abort");
  
  const { unmount } = renderHook(() => useHook());
  unmount();
  
  expect(abortSpy).toHaveBeenCalled();
  abortSpy.mockRestore();
});
```

## Error Handling Testing

### API Error Testing
```typescript
test("handles API errors gracefully", async () => {
  const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
  vi.mocked(apiCall).mockRejectedValue(new Error("API Error"));

  const { result } = renderHook(() => useHook());

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

  expect(consoleSpy).toHaveBeenCalledWith("Error message:", expect.any(Error));
  expect(result.current.data).toBe(fallbackValue);
  
  consoleSpy.mockRestore();
});
```

### Cancelled Request Testing
```typescript
test("does not update state for cancelled requests", async () => {
  const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});

  let rejectFirst: (error: any) => void;
  const firstPromise = new Promise((_, reject) => {
    rejectFirst = reject;
  });

  vi.mocked(apiCall)
    .mockReturnValueOnce(firstPromise as any)
    .mockResolvedValueOnce({ data: 42 });

  const { result } = renderHook(() => useHook());
  result.current.refetch();

  const abortError = new Error("Request cancelled");
  rejectFirst!(abortError);

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

  // Should not log error for cancelled request
  expect(consoleSpy).not.toHaveBeenCalled();
  consoleSpy.mockRestore();
});
```

## Type Safety in Tests

### Mock Type Assertions
Use type assertions for edge cases:
```typescript
vi.mocked(apiCall).mockResolvedValue({
  data: null as any, // For testing null handling
});

vi.mocked(apiCall).mockResolvedValue({
  data: undefined as any, // For testing undefined handling
});
```

### Proper Mock Typing
Ensure mocks match the actual interface:
```typescript
const mockSurvey: TSurvey = {
  id: "survey-123",
  name: "Test Survey",
  // ... other required properties
} as unknown as TSurvey; // Use when partial mocking is needed
```

## Common Test Patterns

### Testing State Changes
```typescript
test("updates state correctly", async () => {
  const { result } = renderHook(() => useHook());
  
  // Initial state
  expect(result.current.value).toBe(initialValue);
  
  // Trigger change
  result.current.updateValue(newValue);
  
  // Verify change
  expect(result.current.value).toBe(newValue);
});
```

### Testing Multiple Scenarios
```typescript
test("handles different modes", async () => {
  // Test regular mode
  vi.mocked(useParams).mockReturnValue({ surveyId: "123" });
  const { rerender } = renderHook(() => useHook());
  
  await waitFor(() => {
    expect(vi.mocked(regularApi)).toHaveBeenCalled();
  });
  
  // Test sharing mode
  vi.mocked(useParams).mockReturnValue({ 
    surveyId: "123", 
    sharingKey: "share-123" 
  });
  rerender();
  
  await waitFor(() => {
    expect(vi.mocked(sharingApi)).toHaveBeenCalled();
  });
});
```

## Test Organization

### Comprehensive Test Coverage
For hooks, ensure you test:
- ✅ Initialization (with/without initial values)
- ✅ Data fetching (success/error cases)
- ✅ State updates and refetching
- ✅ Dependency changes triggering effects
- ✅ Manual actions (refetch, reset)
- ✅ Race condition prevention
- ✅ Cleanup on unmount
- ✅ Mode switching (if applicable)
- ✅ Edge cases (null/undefined data)

### Test Naming
Use descriptive test names that explain the scenario:
- ✅ "initializes with initial count"
- ✅ "fetches response count on mount for regular survey"
- ✅ "cancels previous request when new request is made"
- ❌ "test hook"
- ❌ "it works"
