359 lines
10 KiB
Markdown
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
|
|
```
|