# 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 }) => ( {children} ); }; it('renders component', () => { render(, { 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 ```