All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 25m5s
620 lines
19 KiB
Markdown
620 lines
19 KiB
Markdown
# Testing Guide
|
|
|
|
## Quick Reference
|
|
|
|
| Command | Purpose |
|
|
| ------------------------------------------------------------ | ---------------------------- |
|
|
| `podman exec -it flyer-crawler-dev npm test` | Run all tests |
|
|
| `podman exec -it flyer-crawler-dev npm run test:unit` | Unit tests (~2900) |
|
|
| `podman exec -it flyer-crawler-dev npm run test:integration` | Integration tests (28 files) |
|
|
| `podman exec -it flyer-crawler-dev npm run test:e2e` | E2E tests (11 files) |
|
|
| `podman exec -it flyer-crawler-dev npm run type-check` | TypeScript check |
|
|
|
|
**Critical**: Always run tests in the dev container. Windows results are unreliable.
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
This project has comprehensive test coverage including unit tests, integration tests, and E2E tests. All tests must be run in the **Linux dev container environment** for reliable results.
|
|
|
|
## Test Execution Environment
|
|
|
|
**CRITICAL**: All tests and type-checking MUST be executed inside the dev container (Linux environment).
|
|
|
|
### Why Linux Only?
|
|
|
|
- Path separators: Code uses POSIX-style paths (`/`) which may break on Windows
|
|
- TypeScript compilation works differently on Windows vs Linux
|
|
- Shell scripts and external dependencies assume Linux
|
|
- Test results from Windows are **unreliable and should be ignored**
|
|
|
|
### Running Tests Correctly
|
|
|
|
#### Option 1: Inside Dev Container (Recommended)
|
|
|
|
Open VS Code and use "Reopen in Container", then:
|
|
|
|
```bash
|
|
npm test # Run all tests
|
|
npm run test:unit # Run unit tests only
|
|
npm run test:integration # Run integration tests
|
|
npm run type-check # Run TypeScript type checking
|
|
```
|
|
|
|
#### Option 2: Via Podman from Windows Host
|
|
|
|
From the Windows host, execute commands in the container:
|
|
|
|
```bash
|
|
# Run 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
|
|
|
|
# Run integration tests
|
|
podman exec -it flyer-crawler-dev npm run test:integration
|
|
|
|
# Run type checking
|
|
podman exec -it flyer-crawler-dev npm run type-check
|
|
|
|
# Run specific test file
|
|
podman exec -it flyer-crawler-dev npm test -- --run src/hooks/useAuth.test.tsx
|
|
```
|
|
|
|
## Type Checking
|
|
|
|
TypeScript type checking is performed using `tsc --noEmit`.
|
|
|
|
### Type Check Command
|
|
|
|
```bash
|
|
npm run type-check
|
|
```
|
|
|
|
### Type Check Validation
|
|
|
|
The type-check command will:
|
|
|
|
- Exit with code 0 if no errors are found
|
|
- Exit with non-zero code and print errors if type errors exist
|
|
- Check all files in the `src/` directory as defined in `tsconfig.json`
|
|
|
|
**IMPORTANT**: Type-check on Windows may not show errors reliably. Always verify type-check results by running in the dev container.
|
|
|
|
### Verifying Type Check Works
|
|
|
|
To verify type-check is working correctly:
|
|
|
|
1. Run type-check in dev container: `podman exec -it flyer-crawler-dev npm run type-check`
|
|
2. Check for output - errors will be displayed with file paths and line numbers
|
|
3. No output + exit code 0 = no type errors
|
|
|
|
Example error output:
|
|
|
|
```text
|
|
src/pages/MyDealsPage.tsx:68:31 - error TS2339: Property 'store_name' does not exist on type 'WatchedItemDeal'.
|
|
|
|
68 <span>{deal.store_name}</span>
|
|
~~~~~~~~~~
|
|
```
|
|
|
|
## Pre-Commit Hooks
|
|
|
|
The project uses Husky and lint-staged for pre-commit validation:
|
|
|
|
```bash
|
|
# .husky/pre-commit
|
|
npx lint-staged
|
|
```
|
|
|
|
Lint-staged configuration (`.lintstagedrc.json`):
|
|
|
|
```json
|
|
{
|
|
"*.{js,jsx,ts,tsx}": ["eslint --fix --no-color", "prettier --write"],
|
|
"*.{json,md,css,html,yml,yaml}": ["prettier --write"]
|
|
}
|
|
```
|
|
|
|
**Note**: The `--no-color` flag prevents ANSI color codes from breaking file path links in git output.
|
|
|
|
## Test Suite Structure
|
|
|
|
### Unit Tests (~2900 tests)
|
|
|
|
Located throughout `src/` directory alongside source files with `.test.ts` or `.test.tsx` extensions.
|
|
|
|
```bash
|
|
npm run test:unit
|
|
```
|
|
|
|
### Integration Tests (28 test files)
|
|
|
|
Located in `src/tests/integration/`. Key test files include:
|
|
|
|
| Test File | Domain |
|
|
| -------------------------------------- | -------------------------- |
|
|
| `admin.integration.test.ts` | Admin dashboard operations |
|
|
| `auth.integration.test.ts` | Authentication flows |
|
|
| `budget.integration.test.ts` | Budget management |
|
|
| `flyer.integration.test.ts` | Flyer CRUD operations |
|
|
| `flyer-processing.integration.test.ts` | AI flyer processing |
|
|
| `gamification.integration.test.ts` | Achievements and points |
|
|
| `inventory.integration.test.ts` | Inventory management |
|
|
| `notification.integration.test.ts` | User notifications |
|
|
| `receipt.integration.test.ts` | Receipt processing |
|
|
| `recipe.integration.test.ts` | Recipe management |
|
|
| `shopping-list.integration.test.ts` | Shopping list operations |
|
|
| `user.integration.test.ts` | User profile operations |
|
|
|
|
See `src/tests/integration/` for the complete list.
|
|
|
|
Requires PostgreSQL and Redis services running.
|
|
|
|
```bash
|
|
npm run test:integration
|
|
```
|
|
|
|
### E2E Tests (11 test files)
|
|
|
|
Located in `src/tests/e2e/`. Full user journey tests:
|
|
|
|
| Test File | Journey |
|
|
| --------------------------------- | ----------------------------- |
|
|
| `admin-authorization.e2e.test.ts` | Admin access control |
|
|
| `admin-dashboard.e2e.test.ts` | Admin dashboard flows |
|
|
| `auth.e2e.test.ts` | Login/logout/registration |
|
|
| `budget-journey.e2e.test.ts` | Budget tracking workflow |
|
|
| `deals-journey.e2e.test.ts` | Finding and saving deals |
|
|
| `error-reporting.e2e.test.ts` | Error handling verification |
|
|
| `flyer-upload.e2e.test.ts` | Flyer upload and processing |
|
|
| `inventory-journey.e2e.test.ts` | Pantry management |
|
|
| `receipt-journey.e2e.test.ts` | Receipt scanning and tracking |
|
|
| `upc-journey.e2e.test.ts` | UPC barcode scanning |
|
|
| `user-journey.e2e.test.ts` | User profile management |
|
|
|
|
Requires all services (PostgreSQL, Redis, BullMQ workers) running.
|
|
|
|
```bash
|
|
npm run test:e2e
|
|
```
|
|
|
|
## Test Result Interpretation
|
|
|
|
- Tests that **pass on Windows but fail on Linux** = **BROKEN tests** (must be fixed)
|
|
- Tests that **fail on Windows but pass on Linux** = **PASSING tests** (acceptable)
|
|
- Always use **Linux (dev container) results** as the source of truth
|
|
|
|
## Test Helpers
|
|
|
|
### Store Test Helpers
|
|
|
|
Located in `src/tests/utils/storeHelpers.ts`:
|
|
|
|
```typescript
|
|
// Create a store with a location in one call
|
|
const store = await createStoreWithLocation(pool, {
|
|
name: 'Test Store',
|
|
address: '123 Main St',
|
|
city: 'Toronto',
|
|
province: 'ON',
|
|
postalCode: 'M1M 1M1',
|
|
});
|
|
|
|
// Returns: { storeId, addressId, storeLocationId }
|
|
|
|
// Cleanup stores and their locations
|
|
await cleanupStoreLocation(pool, store);
|
|
```
|
|
|
|
### Mock Factories
|
|
|
|
Located in `src/tests/utils/mockFactories.ts`:
|
|
|
|
```typescript
|
|
// Create mock data for tests
|
|
const mockStore = createMockStore({ name: 'Test Store' });
|
|
const mockAddress = createMockAddress({ city: 'Toronto' });
|
|
const mockStoreLocation = createMockStoreLocationWithAddress();
|
|
const mockStoreWithLocations = createMockStoreWithLocations({
|
|
locations: [{ address: { city: 'Toronto' } }],
|
|
});
|
|
```
|
|
|
|
### Test Assets
|
|
|
|
Test images and other assets are located in `src/tests/assets/`:
|
|
|
|
| File | Purpose |
|
|
| ---------------------- | ---------------------------------------------- |
|
|
| `test-flyer-image.jpg` | Sample flyer image for upload/processing tests |
|
|
| `test-flyer-icon.png` | Sample flyer icon (64x64) for thumbnail tests |
|
|
|
|
These images are copied to `public/flyer-images/` by the seed script (`npm run seed`) and served via NGINX at `/flyer-images/`.
|
|
|
|
## Known Integration Test Issues
|
|
|
|
See `CLAUDE.md` for documentation of common integration test issues and their solutions, including:
|
|
|
|
1. Vitest globalSetup context isolation
|
|
2. BullMQ cleanup queue timing issues
|
|
3. Cache invalidation after direct database inserts
|
|
4. Unique filename requirements for file uploads
|
|
5. Response format mismatches
|
|
6. External service availability
|
|
|
|
## Continuous Integration
|
|
|
|
Tests run automatically on:
|
|
|
|
- Pre-commit (via Husky hooks)
|
|
- Pull request creation/update (via Gitea CI/CD)
|
|
- Merge to main branch (via Gitea CI/CD)
|
|
|
|
CI/CD configuration:
|
|
|
|
- `.gitea/workflows/deploy-to-prod.yml`
|
|
- `.gitea/workflows/deploy-to-test.yml`
|
|
|
|
## Coverage Reports
|
|
|
|
Test coverage is tracked using Vitest's built-in coverage tools.
|
|
|
|
```bash
|
|
npm run test:coverage
|
|
```
|
|
|
|
Coverage reports are generated in the `coverage/` directory.
|
|
|
|
## Debugging Tests
|
|
|
|
### Enable Verbose Logging
|
|
|
|
```bash
|
|
# Run tests with verbose output
|
|
npm test -- --reporter=verbose
|
|
|
|
# Run specific test with logging
|
|
DEBUG=* npm test -- --run src/path/to/test.test.ts
|
|
```
|
|
|
|
### Using Vitest UI
|
|
|
|
```bash
|
|
npm run test:ui
|
|
```
|
|
|
|
Opens a browser-based test runner with filtering and debugging capabilities.
|
|
|
|
## Best Practices
|
|
|
|
1. **Always run tests in dev container** - never trust Windows test results
|
|
2. **Run type-check before committing** - catches TypeScript errors early
|
|
3. **Use test helpers** - `createStoreWithLocation()`, mock factories, etc.
|
|
4. **Clean up test data** - use cleanup helpers in `afterEach`/`afterAll`
|
|
5. **Verify cache invalidation** - tests that insert data directly must invalidate cache
|
|
6. **Use unique filenames** - file upload tests need timestamp-based filenames
|
|
7. **Check exit codes** - `npm run type-check` returns 0 on success, non-zero on error
|
|
8. **Use `req.originalUrl` in error logs** - never hardcode API paths in error messages
|
|
9. **Use versioned API paths** - always use `/api/v1/` prefix in test requests
|
|
10. **Use `vi.hoisted()` for module mocks** - ensure mocks are available during module initialization
|
|
|
|
## Testing Error Log Messages
|
|
|
|
When testing route error handlers, ensure assertions account for versioned API paths.
|
|
|
|
### Problem: Hardcoded Paths Break Tests
|
|
|
|
Error log messages with hardcoded paths cause test failures when API versions change:
|
|
|
|
```typescript
|
|
// Production code (INCORRECT - hardcoded path)
|
|
req.log.error({ error }, 'Error in /api/flyers/:id:');
|
|
|
|
// Test expects versioned path
|
|
expect(logSpy).toHaveBeenCalledWith(
|
|
expect.objectContaining({ error: expect.any(Error) }),
|
|
expect.stringContaining('/api/v1/flyers'), // FAILS - actual log has /api/flyers
|
|
);
|
|
```
|
|
|
|
### Solution: Dynamic Paths with `req.originalUrl`
|
|
|
|
Production code should use `req.originalUrl` for dynamic path logging:
|
|
|
|
```typescript
|
|
// Production code (CORRECT - dynamic path)
|
|
req.log.error({ error }, `Error in ${req.originalUrl.split('?')[0]}:`);
|
|
```
|
|
|
|
### Writing Robust Test Assertions
|
|
|
|
```typescript
|
|
// Good - matches versioned path
|
|
expect(logSpy).toHaveBeenCalledWith(
|
|
expect.objectContaining({ error: expect.any(Error) }),
|
|
expect.stringContaining('/api/v1/flyers'),
|
|
);
|
|
|
|
// Good - flexible match for any version
|
|
expect(logSpy).toHaveBeenCalledWith(
|
|
expect.objectContaining({ error: expect.any(Error) }),
|
|
expect.stringMatching(/\/api\/v\d+\/flyers/),
|
|
);
|
|
|
|
// Bad - hardcoded unversioned path
|
|
expect(logSpy).toHaveBeenCalledWith(
|
|
expect.objectContaining({ error: expect.any(Error) }),
|
|
'Error in /api/flyers:', // Will fail with versioned routes
|
|
);
|
|
```
|
|
|
|
See [Error Logging Path Patterns](ERROR-LOGGING-PATHS.md) for complete documentation.
|
|
|
|
## API Versioning in Tests (ADR-008, ADR-057)
|
|
|
|
All API endpoints use the `/api/v1/` prefix. Tests must use versioned paths.
|
|
|
|
### Configuration
|
|
|
|
API base URLs are configured centrally in Vitest config files:
|
|
|
|
| Config File | Environment Variable | Value |
|
|
| ------------------------------ | -------------------- | ------------------------------ |
|
|
| `vite.config.ts` | `VITE_API_BASE_URL` | `/api/v1` |
|
|
| `vitest.config.e2e.ts` | `VITE_API_BASE_URL` | `http://localhost:3098/api/v1` |
|
|
| `vitest.config.integration.ts` | `VITE_API_BASE_URL` | `http://localhost:3099/api/v1` |
|
|
|
|
### Writing API Tests
|
|
|
|
```typescript
|
|
// Good - versioned path
|
|
const response = await request.post('/api/v1/auth/login').send({...});
|
|
|
|
// Bad - unversioned path (will fail)
|
|
const response = await request.post('/api/auth/login').send({...});
|
|
```
|
|
|
|
### Migration Checklist
|
|
|
|
When API version changes (e.g., v1 to v2):
|
|
|
|
1. Update all Vitest config `VITE_API_BASE_URL` values
|
|
2. Search and replace API paths in E2E tests: `grep -r "/api/v1/" src/tests/e2e/`
|
|
3. Search and replace API paths in integration tests
|
|
4. Verify route handler error logs use `req.originalUrl`
|
|
5. Run full test suite in dev container
|
|
|
|
See [ADR-057](../adr/0057-test-remediation-post-api-versioning.md) for complete migration guidance.
|
|
|
|
## vi.hoisted() Pattern for Module Mocks
|
|
|
|
When mocking modules that are imported at module initialization time (like queues or database connections), use `vi.hoisted()` to ensure mocks are available during hoisting.
|
|
|
|
### Problem: Mock Not Available During Import
|
|
|
|
```typescript
|
|
// BAD: Mock might not be ready when module imports it
|
|
vi.mock('../services/queues.server', () => ({
|
|
flyerQueue: { getJobCounts: vi.fn() }, // May not exist yet
|
|
}));
|
|
|
|
import healthRouter from './health.routes'; // Imports queues.server
|
|
```
|
|
|
|
### Solution: Use vi.hoisted()
|
|
|
|
```typescript
|
|
// GOOD: Mocks are created during hoisting, before vi.mock runs
|
|
const { mockQueuesModule } = vi.hoisted(() => {
|
|
const createMockQueue = () => ({
|
|
getJobCounts: vi.fn().mockResolvedValue({
|
|
waiting: 0,
|
|
active: 0,
|
|
failed: 0,
|
|
delayed: 0,
|
|
}),
|
|
});
|
|
|
|
return {
|
|
mockQueuesModule: {
|
|
flyerQueue: createMockQueue(),
|
|
emailQueue: createMockQueue(),
|
|
// ... additional queues
|
|
},
|
|
};
|
|
});
|
|
|
|
// Now the mock object exists when vi.mock factory runs
|
|
vi.mock('../services/queues.server', () => mockQueuesModule);
|
|
|
|
// Safe to import after mocks are defined
|
|
import healthRouter from './health.routes';
|
|
```
|
|
|
|
See [ADR-057](../adr/0057-test-remediation-post-api-versioning.md) for additional patterns.
|
|
|
|
## Testing Role-Based Component Visibility
|
|
|
|
When testing components that render differently based on user roles:
|
|
|
|
### Pattern: Separate Test Cases by Role
|
|
|
|
```typescript
|
|
describe('for authenticated users', () => {
|
|
beforeEach(() => {
|
|
mockedUseAuth.mockReturnValue({
|
|
authStatus: 'AUTHENTICATED',
|
|
userProfile: createMockUserProfile({ role: 'user' }),
|
|
});
|
|
});
|
|
|
|
it('renders user-accessible components', () => {
|
|
render(<MyComponent />);
|
|
expect(screen.getByTestId('user-component')).toBeInTheDocument();
|
|
// Admin-only should NOT be present
|
|
expect(screen.queryByTestId('admin-only')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('for admin users', () => {
|
|
beforeEach(() => {
|
|
mockedUseAuth.mockReturnValue({
|
|
authStatus: 'AUTHENTICATED',
|
|
userProfile: createMockUserProfile({ role: 'admin' }),
|
|
});
|
|
});
|
|
|
|
it('renders admin-only components', () => {
|
|
render(<MyComponent />);
|
|
expect(screen.getByTestId('admin-only')).toBeInTheDocument();
|
|
});
|
|
});
|
|
```
|
|
|
|
### Key Points
|
|
|
|
1. Create separate `describe` blocks for each role
|
|
2. Set up role-specific mocks in `beforeEach`
|
|
3. Test both presence AND absence of role-gated components
|
|
4. Use `screen.queryByTestId()` for elements that should NOT exist
|
|
|
|
## CSS Class Assertions After UI Refactors
|
|
|
|
After frontend style changes, update test assertions to match new CSS classes.
|
|
|
|
### Handling Tailwind Class Changes
|
|
|
|
```typescript
|
|
// Before refactor
|
|
expect(selectedItem).toHaveClass('ring-2', 'ring-brand-primary');
|
|
|
|
// After refactor - update to new classes
|
|
expect(selectedItem).toHaveClass('border-brand-primary', 'bg-teal-50/50');
|
|
```
|
|
|
|
### Flexible Matching
|
|
|
|
For complex class combinations, consider partial matching:
|
|
|
|
```typescript
|
|
// Check for key classes, ignore utility classes
|
|
expect(element).toHaveClass('border-brand-primary');
|
|
|
|
// Or use regex for patterns
|
|
expect(element.className).toMatch(/dark:bg-teal-\d+/);
|
|
```
|
|
|
|
See [ADR-057](../adr/0057-test-remediation-post-api-versioning.md) for lessons learned from the test remediation effort.
|
|
|
|
## TypeScript Type Safety in Tests (ADR-060)
|
|
|
|
Tests must be fully type-safe. Common patterns for handling API response types and mock casting are documented below.
|
|
|
|
### Response Type Narrowing
|
|
|
|
API responses use discriminated unions (`ApiSuccessResponse<T> | ApiErrorResponse`). Access `.data` only after type narrowing.
|
|
|
|
**Utility Functions** (`src/tests/utils/testHelpers.ts`):
|
|
|
|
```typescript
|
|
import { asSuccessResponse, asErrorResponse } from '@/tests/utils/testHelpers';
|
|
|
|
// Success response access
|
|
const response = await request.get('/api/v1/users/1');
|
|
const body = asSuccessResponse<User>(response.body);
|
|
expect(body.data.id).toBe(1);
|
|
|
|
// Error response access
|
|
const errorResponse = await request.post('/api/v1/users').send({});
|
|
expect(errorResponse.status).toBe(400);
|
|
const errorBody = asErrorResponse(errorResponse.body);
|
|
expect(errorBody.error.code).toBe('VALIDATION_ERROR');
|
|
```
|
|
|
|
### Mock Object Type Casting
|
|
|
|
Use appropriate casting based on type compatibility:
|
|
|
|
```typescript
|
|
// Level 1: Type assertion for compatible shapes
|
|
const mock = createMockUser() as User;
|
|
|
|
// Level 2: Unknown bridge for incompatible shapes
|
|
const mock = partialMock as unknown as User;
|
|
|
|
// Level 3: Partial with required overrides
|
|
const mock: User = { ...createPartialUser(), id: 1, email: 'test@test.com' };
|
|
```
|
|
|
|
### Mock Function Casting
|
|
|
|
```typescript
|
|
import { asMock } from '@/tests/utils/testHelpers';
|
|
|
|
// Cast vi.fn() to specific function type
|
|
const mockFn = vi.fn();
|
|
someService.register(asMock<UserService['create']>(mockFn));
|
|
|
|
// vi.fn() with explicit type parameters
|
|
const mockFn = vi.fn<[string], Promise<User>>().mockResolvedValue(mockUser);
|
|
|
|
// vi.mocked() for mocked modules
|
|
vi.mock('@/services/userService');
|
|
const mockedService = vi.mocked(userService);
|
|
mockedService.create.mockResolvedValue(mockUser);
|
|
```
|
|
|
|
### Mock Logger for Controller Tests
|
|
|
|
Controllers require a Pino logger on `req.log`. Use the shared mock logger utility:
|
|
|
|
```typescript
|
|
import { createMockLogger } from '@/tests/utils/testHelpers';
|
|
|
|
function createMockRequest(overrides = {}): ExpressRequest {
|
|
return {
|
|
body: {},
|
|
cookies: {},
|
|
log: createMockLogger(),
|
|
res: { cookie: vi.fn() } as unknown as ExpressResponse,
|
|
...overrides,
|
|
} as unknown as ExpressRequest;
|
|
}
|
|
```
|
|
|
|
The `createMockLogger()` function returns a complete Pino logger mock with all methods (`info`, `debug`, `error`, `warn`, `fatal`, `trace`, `silent`, `child`) as `vi.fn()` mocks.
|
|
|
|
### MSW Handler Typing
|
|
|
|
Ensure MSW handlers return properly typed API responses:
|
|
|
|
```typescript
|
|
import { ApiSuccessResponse } from '@/types/api';
|
|
import { Flyer } from '@/types/flyer';
|
|
|
|
http.get('/api/v1/flyers', () => {
|
|
const response: ApiSuccessResponse<Flyer[]> = {
|
|
success: true,
|
|
data: [mockFlyer],
|
|
};
|
|
return HttpResponse.json(response);
|
|
});
|
|
```
|
|
|
|
### Generic Type Parameters
|
|
|
|
Provide explicit generics when TypeScript cannot infer:
|
|
|
|
```typescript
|
|
// Factory function generic
|
|
const mock = createMockPaginatedResponse<Flyer>({ data: [mockFlyer] });
|
|
|
|
// Assertion generic
|
|
expect(result).toEqual<ApiSuccessResponse<User>>({
|
|
success: true,
|
|
data: mockUser,
|
|
});
|
|
```
|
|
|
|
See [ADR-060](../adr/0060-typescript-test-error-remediation.md) for comprehensive patterns and remediation strategies.
|