Files
flyer-crawler.projectium.com/docs/SUBAGENT-TESTER-REFERENCE.md

359 lines
10 KiB
Markdown

# Tester Subagent Reference
## Critical Rule: Linux Only (ADR-014)
**ALL tests MUST run in the dev container.** Windows test results are unreliable.
| Result | Interpretation |
| ------------------------- | -------------------- |
| Pass Windows / Fail Linux | BROKEN - must fix |
| Fail Windows / Pass Linux | PASSING - acceptable |
---
## Test Commands
### From Windows Host (via Podman)
```bash
# Unit tests (~2900 tests) - pipe to file for AI processing
podman exec -it flyer-crawler-dev npm run test:unit 2>&1 | tee test-results.txt
# Integration tests (requires DB/Redis)
podman exec -it flyer-crawler-dev npm run test:integration
# E2E tests (requires all services)
podman exec -it flyer-crawler-dev npm run test:e2e
# Specific test file
podman exec -it flyer-crawler-dev npm test -- --run src/hooks/useAuth.test.tsx
# Type checking (CRITICAL before commit)
podman exec -it flyer-crawler-dev npm run type-check
# Coverage report
podman exec -it flyer-crawler-dev npm run test:coverage
```
### Inside Dev Container
```bash
npm test # All tests
npm run test:unit # Unit tests only
npm run test:integration # Integration tests
npm run test:e2e # E2E tests
npm run type-check # TypeScript check
```
---
## Test File Locations
| Test Type | Location | Config |
| ----------- | --------------------------------------------- | ------------------------------ |
| Unit | `src/**/*.test.ts`, `src/**/*.test.tsx` | `vite.config.ts` |
| Integration | `src/tests/integration/*.integration.test.ts` | `vitest.config.integration.ts` |
| E2E | `src/tests/e2e/*.e2e.test.ts` | `vitest.config.e2e.ts` |
| Setup | `src/tests/setup/*.ts` | - |
| Helpers | `src/tests/utils/*.ts` | - |
---
## Test Helpers
### Location: `src/tests/utils/`
| Helper | Purpose | Import |
| ----------------------- | --------------------------------------------------------------- | ----------------------------------- |
| `testHelpers.ts` | `createAndLoginUser()`, `getTestBaseUrl()`, `getFlyerBaseUrl()` | `../tests/utils/testHelpers` |
| `cleanup.ts` | `cleanupDb({ userIds, flyerIds })` | `../tests/utils/cleanup` |
| `mockFactories.ts` | `createMockStore()`, `createMockAddress()`, `createMockFlyer()` | `../tests/utils/mockFactories` |
| `storeHelpers.ts` | `createStoreWithLocation()`, `cleanupStoreLocations()` | `../tests/utils/storeHelpers` |
| `poll.ts` | `poll(fn, predicate, options)` - wait for async conditions | `../tests/utils/poll` |
| `mockLogger.ts` | Mock pino logger for tests | `../tests/utils/mockLogger` |
| `createTestApp.ts` | Create Express app instance for route tests | `../tests/utils/createTestApp` |
| `createMockRequest.ts` | Create mock Express request objects | `../tests/utils/createMockRequest` |
| `cleanupFiles.ts` | Clean up test file uploads | `../tests/utils/cleanupFiles` |
| `websocketTestUtils.ts` | WebSocket testing utilities | `../tests/utils/websocketTestUtils` |
### Usage Examples
```typescript
// Create authenticated user for tests
import { createAndLoginUser, TEST_PASSWORD } from '../tests/utils/testHelpers';
const { user, token } = await createAndLoginUser({
email: `test-${Date.now()}@example.com`,
request: request(app), // For integration tests
role: 'admin', // Optional: make admin
});
// Cleanup after tests
import { cleanupDb } from '../tests/utils/cleanup';
afterEach(async () => {
await cleanupDb({ userIds: [user.user.user_id] });
});
// Wait for async operation
import { poll } from '../tests/utils/poll';
await poll(
() => db.userRepo.findUserByEmail(email, logger),
(user) => !!user,
{ timeout: 5000, interval: 500, description: 'user to be findable' },
);
// Create mock data
import { createMockStore, createMockFlyer } from '../tests/utils/mockFactories';
const mockStore = createMockStore({ name: 'Test Store' });
const mockFlyer = createMockFlyer({ store_id: mockStore.store_id });
```
---
## Test Setup Files
| File | Purpose |
| --------------------------------------------- | ---------------------------------------- |
| `src/tests/setup/tests-setup-unit.ts` | Unit test setup (mocks, DOM environment) |
| `src/tests/setup/tests-setup-integration.ts` | Integration test setup (DB connections) |
| `src/tests/setup/global-setup.ts` | Global setup for unit tests |
| `src/tests/setup/integration-global-setup.ts` | Global setup for integration tests |
| `src/tests/setup/e2e-global-setup.ts` | Global setup for E2E tests |
| `src/tests/setup/mockHooks.ts` | React hook mocking utilities |
| `src/tests/setup/mockUI.ts` | UI component mocking |
| `src/tests/setup/globalApiMock.ts` | API mocking setup |
---
## Known Integration Test Issues
### 1. Vitest globalSetup Context Isolation
**Problem**: globalSetup runs in separate Node.js context. Singletons/mocks don't share instances.
**Affected**: BullMQ worker mocks
**Solution**: Use `.todo()`, test-only API endpoints, or Redis-based flags.
### 2. Cleanup Queue Race Condition
**Problem**: Cleanup worker processes jobs before test verification.
**Solution**:
```typescript
const { cleanupQueue } = await import('../../services/queues.server');
await cleanupQueue.drain();
await cleanupQueue.pause();
// ... run test ...
await cleanupQueue.resume();
```
### 3. Cache Stale After Direct SQL
**Problem**: Direct `pool.query()` bypasses cache invalidation.
**Solution**:
```typescript
await pool.query('INSERT INTO flyers ...');
await cacheService.invalidateFlyers(); // Add this
```
### 4. File Upload Filename Collisions
**Problem**: Multer predictable filenames cause race conditions.
**Solution**:
```typescript
const filename = `test-${Date.now()}-${Math.round(Math.random() * 1e9)}.jpg`;
```
### 5. Response Format Mismatches
**Problem**: API response structure changes (`data.jobId` vs `data.job.id`).
**Solution**: Log response bodies, update assertions to match actual format.
---
## Test Patterns
### Unit Test Pattern
```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
describe('MyService', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should do something', () => {
// Arrange
const input = { data: 'test' };
// Act
const result = myService.process(input);
// Assert
expect(result).toBe('expected');
});
});
```
### Integration Test Pattern
```typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import request from 'supertest';
import { createAndLoginUser } from '../utils/testHelpers';
import { cleanupDb } from '../utils/cleanup';
describe('API Integration', () => {
let user, token;
beforeEach(async () => {
const result = await createAndLoginUser({ request: request(app) });
user = result.user;
token = result.token;
});
afterEach(async () => {
await cleanupDb({ userIds: [user.user.user_id] });
});
it('GET /api/resource returns data', async () => {
const res = await request(app).get('/api/resource').set('Authorization', `Bearer ${token}`);
expect(res.status).toBe(200);
expect(res.body.success).toBe(true);
});
});
```
### Route Test Pattern
```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import request from 'supertest';
import { createTestApp } from '../tests/utils/createTestApp';
vi.mock('../services/db/flyer.db', () => ({
getFlyerById: vi.fn(),
}));
describe('Flyer Routes', () => {
let app;
beforeEach(() => {
vi.clearAllMocks();
app = createTestApp();
});
it('GET /api/flyers/:id returns flyer', async () => {
const mockFlyer = { id: '123', name: 'Test' };
vi.mocked(flyerRepo.getFlyerById).mockResolvedValue(mockFlyer);
const res = await request(app).get('/api/flyers/123');
expect(res.status).toBe(200);
expect(res.body.data).toEqual(mockFlyer);
});
});
```
---
## Mocking Patterns
### Mock Modules
```typescript
// At top of test file
vi.mock('../services/db/flyer.db', () => ({
getFlyerById: vi.fn(),
listFlyers: vi.fn(),
}));
// In test
import * as flyerDb from '../services/db/flyer.db';
vi.mocked(flyerDb.getFlyerById).mockResolvedValue(mockFlyer);
```
### Mock React Query
```typescript
vi.mock('@tanstack/react-query', async () => {
const actual = await vi.importActual('@tanstack/react-query');
return {
...actual,
useQuery: vi.fn().mockReturnValue({
data: mockData,
isLoading: false,
error: null,
}),
};
});
```
### Mock Pino Logger
```typescript
import { createMockLogger } from '../tests/utils/mockLogger';
const mockLogger = createMockLogger();
```
---
## Testing React Components
```typescript
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter } from 'react-router-dom';
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return ({ children }) => (
<QueryClientProvider client={queryClient}>
<BrowserRouter>{children}</BrowserRouter>
</QueryClientProvider>
);
};
it('renders component', () => {
render(<MyComponent />, { wrapper: createWrapper() });
expect(screen.getByText('Expected Text')).toBeInTheDocument();
});
```
---
## Test Coverage
```bash
# Generate coverage report
podman exec -it flyer-crawler-dev npm run test:coverage
# View HTML report
# Coverage reports generated in coverage/ directory
```
---
## Debugging Tests
```bash
# Verbose output
npm test -- --reporter=verbose
# Run single test with debugging
DEBUG=* npm test -- --run src/path/to/test.test.ts
# Vitest UI (interactive)
npm run test:ui
```