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

10 KiB

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)

# 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

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

// 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:

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:

await pool.query('INSERT INTO flyers ...');
await cacheService.invalidateFlyers(); // Add this

4. File Upload Filename Collisions

Problem: Multer predictable filenames cause race conditions.

Solution:

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

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

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

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

// 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

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

import { createMockLogger } from '../tests/utils/mockLogger';
const mockLogger = createMockLogger();

Testing React Components

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

# Generate coverage report
podman exec -it flyer-crawler-dev npm run test:coverage

# View HTML report
# Coverage reports generated in coverage/ directory

Debugging Tests

# 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