Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23830c0d4e | ||
| ef42fee982 |
@@ -57,7 +57,9 @@
|
||||
"mcp__sequential-thinking__sequentialthinking",
|
||||
"mcp__filesystem__list_directory",
|
||||
"mcp__filesystem__read_multiple_files",
|
||||
"mcp__filesystem__directory_tree"
|
||||
"mcp__filesystem__directory_tree",
|
||||
"mcp__filesystem__read_text_file",
|
||||
"Bash(wc:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
**Date**: 2025-12-12
|
||||
|
||||
**Status**: Proposed
|
||||
**Status**: Accepted
|
||||
|
||||
## Context
|
||||
|
||||
@@ -14,9 +14,305 @@ We will formalize the testing pyramid for the project, defining the role of each
|
||||
|
||||
1. **Unit Tests (Vitest)**: For isolated functions, components, and repository methods with mocked dependencies. High coverage is expected.
|
||||
2. **Integration Tests (Supertest)**: For API routes, testing the interaction between controllers, services, and mocked database layers. Focus on contract and middleware correctness.
|
||||
3. **End-to-End (E2E) Tests (Playwright/Cypress)**: For critical user flows (e.g., login, flyer upload, checkout), running against a real browser and a test database to ensure the entire system works together.
|
||||
3. **End-to-End (E2E) Tests (Vitest + Supertest)**: For critical user flows (e.g., login, flyer upload, checkout), running against a real test server and database to ensure the entire system works together.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive**: Ensures a consistent and comprehensive approach to quality assurance. Gives developers confidence when refactoring or adding new features. Clearly defines "done" for a new feature.
|
||||
**Negative**: May require investment in setting up and maintaining the E2E testing environment. Can slightly increase the time required to develop a feature if all test layers are required.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Testing Framework Stack
|
||||
|
||||
| Tool | Version | Purpose |
|
||||
| ---- | ------- | ------- |
|
||||
| Vitest | 4.0.15 | Test runner for all test types |
|
||||
| @testing-library/react | 16.3.0 | React component testing |
|
||||
| @testing-library/jest-dom | 6.9.1 | DOM assertion matchers |
|
||||
| supertest | 7.1.4 | HTTP assertion library for API testing |
|
||||
| msw | 2.12.3 | Mock Service Worker for network mocking |
|
||||
| testcontainers | 11.8.1 | Database containerization (optional) |
|
||||
| c8 + nyc | 10.1.3 / 17.1.0 | Coverage reporting |
|
||||
|
||||
### Test File Organization
|
||||
|
||||
```text
|
||||
src/
|
||||
├── components/
|
||||
│ └── *.test.tsx # Component unit tests (colocated)
|
||||
├── hooks/
|
||||
│ └── *.test.ts # Hook unit tests (colocated)
|
||||
├── services/
|
||||
│ └── *.test.ts # Service unit tests (colocated)
|
||||
├── routes/
|
||||
│ └── *.test.ts # Route handler unit tests (colocated)
|
||||
├── utils/
|
||||
│ └── *.test.ts # Utility function tests (colocated)
|
||||
└── tests/
|
||||
├── setup/ # Test configuration and setup files
|
||||
├── utils/ # Test utilities, factories, helpers
|
||||
├── assets/ # Test fixtures (images, files)
|
||||
├── integration/ # Integration test files (*.test.ts)
|
||||
└── e2e/ # End-to-end test files (*.e2e.test.ts)
|
||||
```
|
||||
|
||||
**Naming Convention**: `{filename}.test.ts` or `{filename}.test.tsx` for unit/integration, `{filename}.e2e.test.ts` for E2E.
|
||||
|
||||
### Configuration Files
|
||||
|
||||
| Config | Environment | Purpose |
|
||||
| ------ | ----------- | ------- |
|
||||
| `vite.config.ts` | jsdom | Unit tests (React components, hooks) |
|
||||
| `vitest.config.integration.ts` | node | Integration tests (API routes) |
|
||||
| `vitest.config.e2e.ts` | node | E2E tests (full user flows) |
|
||||
| `vitest.workspace.ts` | - | Orchestrates all test projects |
|
||||
|
||||
### Test Pyramid
|
||||
|
||||
```text
|
||||
┌─────────────┐
|
||||
│ E2E │ 5 test files
|
||||
│ Tests │ Critical user flows
|
||||
├─────────────┤
|
||||
│ Integration │ 17 test files
|
||||
│ Tests │ API contracts + middleware
|
||||
┌───┴─────────────┴───┐
|
||||
│ Unit Tests │ 185 test files
|
||||
│ Components, Hooks, │ Isolated functions
|
||||
│ Services, Utils │ Mocked dependencies
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
### Unit Tests
|
||||
|
||||
**Purpose**: Test isolated functions, components, and modules with mocked dependencies.
|
||||
|
||||
**Environment**: jsdom (browser-like)
|
||||
|
||||
**Key Patterns**:
|
||||
|
||||
```typescript
|
||||
// Component testing with providers
|
||||
import { renderWithProviders, screen } from '@/tests/utils/renderWithProviders';
|
||||
|
||||
describe('MyComponent', () => {
|
||||
it('renders correctly', () => {
|
||||
renderWithProviders(<MyComponent />);
|
||||
expect(screen.getByText('Hello')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
```typescript
|
||||
// Hook testing
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { useMyHook } from './useMyHook';
|
||||
|
||||
describe('useMyHook', () => {
|
||||
it('returns expected value', async () => {
|
||||
const { result } = renderHook(() => useMyHook());
|
||||
await waitFor(() => expect(result.current.data).toBeDefined());
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Global Mocks** (automatically applied via `tests-setup-unit.ts`):
|
||||
|
||||
- Database connections (`pg.Pool`)
|
||||
- AI services (`@google/genai`)
|
||||
- Authentication (`jsonwebtoken`, `bcrypt`)
|
||||
- Logging (`logger.server`, `logger.client`)
|
||||
- Notifications (`notificationService`)
|
||||
|
||||
### Integration Tests
|
||||
|
||||
**Purpose**: Test API routes with real service interactions and database.
|
||||
|
||||
**Environment**: node
|
||||
|
||||
**Setup**: Real Express server on port 3001, real PostgreSQL database
|
||||
|
||||
```typescript
|
||||
// API route testing pattern
|
||||
import supertest from 'supertest';
|
||||
import { createAndLoginUser } from '@/tests/utils/testHelpers';
|
||||
|
||||
describe('Auth API', () => {
|
||||
let request: ReturnType<typeof supertest>;
|
||||
let authToken: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const app = (await import('../../../server')).default;
|
||||
request = supertest(app);
|
||||
const { token } = await createAndLoginUser(request);
|
||||
authToken = token;
|
||||
});
|
||||
|
||||
it('GET /api/auth/me returns user profile', async () => {
|
||||
const response = await request
|
||||
.get('/api/auth/me')
|
||||
.set('Authorization', `Bearer ${authToken}`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.user.email).toBeDefined();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Database Cleanup**:
|
||||
|
||||
```typescript
|
||||
import { cleanupDb } from '@/tests/utils/cleanup';
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanupDb({ users: [testUserId] });
|
||||
});
|
||||
```
|
||||
|
||||
### E2E Tests
|
||||
|
||||
**Purpose**: Test complete user journeys through the application.
|
||||
|
||||
**Timeout**: 120 seconds (for long-running flows)
|
||||
|
||||
**Current E2E Tests**:
|
||||
|
||||
- `auth.e2e.test.ts` - Registration, login, password reset
|
||||
- `flyer-upload.e2e.test.ts` - Complete flyer upload pipeline
|
||||
- `user-journey.e2e.test.ts` - Full user workflow
|
||||
- `admin-authorization.e2e.test.ts` - Admin-specific flows
|
||||
- `admin-dashboard.e2e.test.ts` - Admin dashboard functionality
|
||||
|
||||
### Mock Factories
|
||||
|
||||
The project uses comprehensive mock factories (`src/tests/utils/mockFactories.ts`, 1553 lines) for creating test data:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
createMockUser,
|
||||
createMockFlyer,
|
||||
createMockFlyerItem,
|
||||
createMockRecipe,
|
||||
resetMockIds,
|
||||
} from '@/tests/utils/mockFactories';
|
||||
|
||||
beforeEach(() => {
|
||||
resetMockIds(); // Ensure deterministic IDs
|
||||
});
|
||||
|
||||
it('creates flyer with items', () => {
|
||||
const flyer = createMockFlyer({ store_name: 'TestMart' });
|
||||
const items = [createMockFlyerItem({ flyer_id: flyer.flyer_id })];
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
**Factory Coverage**: 90+ factory functions for all domain entities including users, flyers, recipes, shopping lists, budgets, achievements, etc.
|
||||
|
||||
### Test Utilities
|
||||
|
||||
| Utility | Purpose |
|
||||
| ------- | ------- |
|
||||
| `renderWithProviders()` | Wrap components with AppProviders + Router |
|
||||
| `createAndLoginUser()` | Create user and return auth token |
|
||||
| `cleanupDb()` | Database cleanup respecting FK constraints |
|
||||
| `createTestApp()` | Create Express app for route testing |
|
||||
| `poll()` | Polling utility for async operations |
|
||||
|
||||
### Coverage Configuration
|
||||
|
||||
**Coverage Provider**: v8 (built-in Vitest)
|
||||
|
||||
**Report Directories**:
|
||||
|
||||
- `.coverage/unit/` - Unit test coverage
|
||||
- `.coverage/integration/` - Integration test coverage
|
||||
- `.coverage/e2e/` - E2E test coverage
|
||||
|
||||
**Excluded from Coverage**:
|
||||
|
||||
- `src/index.tsx`, `src/main.tsx` (entry points)
|
||||
- `src/tests/**` (test files themselves)
|
||||
- `src/**/*.d.ts` (type declarations)
|
||||
- `src/components/icons/**` (icon components)
|
||||
- `src/db/seed*.ts` (database seeding scripts)
|
||||
|
||||
### npm Scripts
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
npm run test
|
||||
|
||||
# Run by level
|
||||
npm run test:unit # Unit tests only (jsdom)
|
||||
npm run test:integration # Integration tests only (node)
|
||||
|
||||
# With coverage
|
||||
npm run test:coverage # Unit + Integration with reports
|
||||
|
||||
# Clean coverage directories
|
||||
npm run clean
|
||||
```
|
||||
|
||||
### Test Timeouts
|
||||
|
||||
| Test Type | Timeout | Rationale |
|
||||
| --------- | ------- | --------- |
|
||||
| Unit | 5 seconds | Fast, isolated tests |
|
||||
| Integration | 60 seconds | AI service calls, DB operations |
|
||||
| E2E | 120 seconds | Full user flow with multiple API calls |
|
||||
|
||||
## Best Practices
|
||||
|
||||
### When to Write Each Test Type
|
||||
|
||||
1. **Unit Tests** (required):
|
||||
- Pure functions and utilities
|
||||
- React components (rendering, user interactions)
|
||||
- Custom hooks
|
||||
- Service methods with mocked dependencies
|
||||
- Repository methods
|
||||
|
||||
2. **Integration Tests** (required for API changes):
|
||||
- New API endpoints
|
||||
- Authentication/authorization flows
|
||||
- Middleware behavior
|
||||
- Database query correctness
|
||||
|
||||
3. **E2E Tests** (for critical paths):
|
||||
- User registration and login
|
||||
- Core business flows (flyer upload, shopping lists)
|
||||
- Admin operations
|
||||
|
||||
### Test Isolation Guidelines
|
||||
|
||||
1. **Reset mock IDs**: Call `resetMockIds()` in `beforeEach()`
|
||||
2. **Unique test data**: Use timestamps or UUIDs for emails/usernames
|
||||
3. **Clean up after tests**: Use `cleanupDb()` in `afterAll()`
|
||||
4. **Don't share state**: Each test should be independent
|
||||
|
||||
### Mocking Guidelines
|
||||
|
||||
1. **Unit tests**: Mock external dependencies (DB, APIs, services)
|
||||
2. **Integration tests**: Mock only external APIs (AI services)
|
||||
3. **E2E tests**: Minimal mocking, use real services where possible
|
||||
|
||||
## Key Files
|
||||
|
||||
- `vite.config.ts` - Unit test configuration
|
||||
- `vitest.config.integration.ts` - Integration test configuration
|
||||
- `vitest.config.e2e.ts` - E2E test configuration
|
||||
- `vitest.workspace.ts` - Workspace orchestration
|
||||
- `src/tests/setup/tests-setup-unit.ts` - Global mocks (488 lines)
|
||||
- `src/tests/setup/integration-global-setup.ts` - Server + DB setup
|
||||
- `src/tests/utils/mockFactories.ts` - Mock factories (1553 lines)
|
||||
- `src/tests/utils/testHelpers.ts` - Test utilities
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Browser E2E Tests**: Consider adding Playwright for actual browser testing
|
||||
2. **Visual Regression**: Screenshot comparison for UI components
|
||||
3. **Performance Testing**: Add benchmarks for critical paths
|
||||
4. **Mutation Testing**: Verify test quality with mutation testing tools
|
||||
5. **Coverage Thresholds**: Define minimum coverage requirements per module
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
**Date**: 2025-12-12
|
||||
|
||||
**Status**: Proposed
|
||||
**Status**: Partially Implemented
|
||||
|
||||
## Context
|
||||
|
||||
@@ -16,3 +16,255 @@ We will establish a formal Design System and Component Library. This will involv
|
||||
|
||||
- **Positive**: Ensures a consistent and high-quality user interface. Accelerates frontend development by providing reusable, well-documented components. Improves maintainability and reduces technical debt.
|
||||
- **Negative**: Requires an initial investment in setting up Storybook and migrating existing components. Adds a new dependency and a new workflow for frontend development.
|
||||
|
||||
## Implementation Status
|
||||
|
||||
### What's Implemented
|
||||
|
||||
The codebase has a solid foundation for a design system:
|
||||
|
||||
- ✅ **Tailwind CSS v4.1.17** as the styling solution
|
||||
- ✅ **Dark mode** fully implemented with system preference detection
|
||||
- ✅ **55 custom icon components** for consistent iconography
|
||||
- ✅ **Component organization** with shared vs. feature-specific separation
|
||||
- ✅ **Accessibility patterns** with ARIA attributes and focus management
|
||||
|
||||
### What's Not Yet Implemented
|
||||
|
||||
- ❌ **Storybook** is not yet installed or configured
|
||||
- ❌ **Formal design token documentation** (colors, typography, spacing)
|
||||
- ❌ **Visual regression testing** for component changes
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Component Library Structure
|
||||
|
||||
```text
|
||||
src/
|
||||
├── components/ # 30+ shared UI components
|
||||
│ ├── icons/ # 55 SVG icon components
|
||||
│ ├── Header.tsx
|
||||
│ ├── Footer.tsx
|
||||
│ ├── LoadingSpinner.tsx
|
||||
│ ├── ErrorDisplay.tsx
|
||||
│ ├── ConfirmationModal.tsx
|
||||
│ ├── DarkModeToggle.tsx
|
||||
│ ├── StatCard.tsx
|
||||
│ ├── PasswordInput.tsx
|
||||
│ └── ...
|
||||
├── features/ # Feature-specific components
|
||||
│ ├── charts/ # PriceChart, PriceHistoryChart
|
||||
│ ├── flyer/ # FlyerDisplay, FlyerList, FlyerUploader
|
||||
│ ├── shopping/ # ShoppingListComponent, WatchedItemsList
|
||||
│ └── voice-assistant/ # VoiceAssistant
|
||||
├── layouts/ # Page layouts
|
||||
│ └── MainLayout.tsx
|
||||
├── pages/ # Page components
|
||||
│ └── admin/components/ # Admin-specific components
|
||||
└── providers/ # Context providers
|
||||
```
|
||||
|
||||
### Styling Approach
|
||||
|
||||
**Tailwind CSS** with utility-first classes:
|
||||
|
||||
```typescript
|
||||
// Component example with consistent styling patterns
|
||||
<button className="px-4 py-2 bg-brand-primary text-white rounded-lg
|
||||
hover:bg-brand-dark transition-colors duration-200
|
||||
focus:outline-none focus:ring-2 focus:ring-brand-primary
|
||||
focus:ring-offset-2 dark:focus:ring-offset-gray-800">
|
||||
Click me
|
||||
</button>
|
||||
```
|
||||
|
||||
**Common Utility Patterns**:
|
||||
|
||||
| Pattern | Classes |
|
||||
| ------- | ------- |
|
||||
| Card container | `bg-white dark:bg-gray-800 rounded-lg shadow-md p-6` |
|
||||
| Primary button | `bg-brand-primary hover:bg-brand-dark text-white rounded-lg px-4 py-2` |
|
||||
| Secondary button | `bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200` |
|
||||
| Input field | `border border-gray-300 dark:border-gray-600 rounded-md px-3 py-2` |
|
||||
| Focus ring | `focus:outline-none focus:ring-2 focus:ring-brand-primary` |
|
||||
|
||||
### Color System
|
||||
|
||||
**Brand Colors** (Tailwind theme extensions):
|
||||
|
||||
- `brand-primary` - Primary brand color (blue/teal)
|
||||
- `brand-light` - Lighter variant
|
||||
- `brand-dark` - Darker variant for hover states
|
||||
- `brand-secondary` - Secondary accent color
|
||||
|
||||
**Semantic Colors**:
|
||||
|
||||
- Gray scale: `gray-50` through `gray-950`
|
||||
- Error: `red-500`, `red-600`
|
||||
- Success: `green-500`, `green-600`
|
||||
- Warning: `yellow-500`, `orange-500`
|
||||
- Info: `blue-500`, `blue-600`
|
||||
|
||||
### Dark Mode Implementation
|
||||
|
||||
Dark mode is fully implemented using Tailwind's `dark:` variant:
|
||||
|
||||
```typescript
|
||||
// Initialization in useAppInitialization hook
|
||||
const initializeDarkMode = () => {
|
||||
// Priority: user profile > localStorage > system preference
|
||||
const stored = localStorage.getItem('darkMode');
|
||||
const systemPreference = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const isDarkMode = stored ? stored === 'true' : systemPreference;
|
||||
|
||||
document.documentElement.classList.toggle('dark', isDarkMode);
|
||||
return isDarkMode;
|
||||
};
|
||||
```
|
||||
|
||||
**Usage in components**:
|
||||
|
||||
```typescript
|
||||
<div className="bg-white dark:bg-gray-800 text-gray-900 dark:text-white">
|
||||
Content adapts to theme
|
||||
</div>
|
||||
```
|
||||
|
||||
### Icon System
|
||||
|
||||
**55 custom SVG icon components** in `src/components/icons/`:
|
||||
|
||||
```typescript
|
||||
// Icon component pattern
|
||||
interface IconProps extends React.SVGProps<SVGSVGElement> {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export const CheckCircleIcon: React.FC<IconProps> = ({ title, ...props }) => (
|
||||
<svg {...props} fill="currentColor" viewBox="0 0 24 24">
|
||||
{title && <title>{title}</title>}
|
||||
<path d="..." />
|
||||
</svg>
|
||||
);
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
|
||||
```typescript
|
||||
<CheckCircleIcon className="w-5 h-5 text-green-500" title="Success" />
|
||||
```
|
||||
|
||||
**External icons**: Lucide React (`lucide-react` v0.555.0) used for additional icons.
|
||||
|
||||
### Accessibility Patterns
|
||||
|
||||
**ARIA Attributes**:
|
||||
|
||||
```typescript
|
||||
// Modal pattern
|
||||
<div role="dialog" aria-modal="true" aria-labelledby="modal-title">
|
||||
<h2 id="modal-title">Modal Title</h2>
|
||||
</div>
|
||||
|
||||
// Button with label
|
||||
<button aria-label="Close modal">
|
||||
<XMarkIcon aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
// Loading state
|
||||
<div role="status" aria-live="polite">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
```
|
||||
|
||||
**Focus Management**:
|
||||
|
||||
- Consistent focus rings: `focus:ring-2 focus:ring-brand-primary focus:ring-offset-2`
|
||||
- Dark mode offset: `dark:focus:ring-offset-gray-800`
|
||||
- No outline: `focus:outline-none` (using ring instead)
|
||||
|
||||
### State Management
|
||||
|
||||
**Context Providers** (see ADR-005):
|
||||
|
||||
| Provider | Purpose |
|
||||
| -------- | ------- |
|
||||
| `AuthProvider` | Authentication state |
|
||||
| `ModalProvider` | Modal open/close state |
|
||||
| `FlyersProvider` | Flyer data |
|
||||
| `MasterItemsProvider` | Grocery items |
|
||||
| `UserDataProvider` | User-specific data |
|
||||
|
||||
**Provider Hierarchy** in `AppProviders.tsx`:
|
||||
|
||||
```typescript
|
||||
<QueryClientProvider>
|
||||
<ModalProvider>
|
||||
<AuthProvider>
|
||||
<FlyersProvider>
|
||||
<MasterItemsProvider>
|
||||
<UserDataProvider>
|
||||
{children}
|
||||
</UserDataProvider>
|
||||
</MasterItemsProvider>
|
||||
</FlyersProvider>
|
||||
</AuthProvider>
|
||||
</ModalProvider>
|
||||
</QueryClientProvider>
|
||||
```
|
||||
|
||||
## Key Files
|
||||
|
||||
- `tailwind.config.js` - Tailwind CSS configuration
|
||||
- `src/index.css` - Tailwind CSS entry point
|
||||
- `src/components/` - Shared UI components
|
||||
- `src/components/icons/` - Icon component library (55 icons)
|
||||
- `src/providers/AppProviders.tsx` - Context provider composition
|
||||
- `src/hooks/useAppInitialization.ts` - Dark mode initialization
|
||||
|
||||
## Component Guidelines
|
||||
|
||||
### When to Create Shared Components
|
||||
|
||||
Create a shared component in `src/components/` when:
|
||||
|
||||
1. Used in 3+ places across the application
|
||||
2. Represents a reusable UI pattern (buttons, cards, modals)
|
||||
3. Has consistent styling/behavior requirements
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- **Components**: PascalCase (`LoadingSpinner.tsx`)
|
||||
- **Icons**: PascalCase with `Icon` suffix (`CheckCircleIcon.tsx`)
|
||||
- **Hooks**: camelCase with `use` prefix (`useModal.ts`)
|
||||
- **Contexts**: PascalCase with `Context` suffix (`AuthContext.tsx`)
|
||||
|
||||
### Styling Guidelines
|
||||
|
||||
1. Use Tailwind utility classes exclusively
|
||||
2. Include dark mode variants for all colors: `bg-white dark:bg-gray-800`
|
||||
3. Add focus states for interactive elements
|
||||
4. Use semantic color names from the design system
|
||||
|
||||
## Future Enhancements (Storybook Setup)
|
||||
|
||||
To complete ADR-012 implementation:
|
||||
|
||||
1. **Install Storybook**:
|
||||
|
||||
```bash
|
||||
npx storybook@latest init
|
||||
```
|
||||
|
||||
2. **Create stories for core components**:
|
||||
- Button variants
|
||||
- Form inputs (PasswordInput, etc.)
|
||||
- Modal components
|
||||
- Loading states
|
||||
- Icon showcase
|
||||
|
||||
3. **Add visual regression testing** with Chromatic or Percy
|
||||
|
||||
4. **Document design tokens** formally in Storybook
|
||||
|
||||
5. **Create component composition guidelines**
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.9.68",
|
||||
"version": "0.9.69",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.9.68",
|
||||
"version": "0.9.69",
|
||||
"dependencies": {
|
||||
"@bull-board/api": "^6.14.2",
|
||||
"@bull-board/express": "^6.14.2",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "flyer-crawler",
|
||||
"private": true,
|
||||
"version": "0.9.68",
|
||||
"version": "0.9.69",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"npm:start:dev\" \"vite\"",
|
||||
|
||||
@@ -18,6 +18,7 @@ describe('Address DB Service', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockDb.query.mockReset();
|
||||
addressRepo = new AddressRepository(mockDb);
|
||||
});
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ describe('Admin DB Service', () => {
|
||||
beforeEach(() => {
|
||||
// Reset the global mock's call history before each test.
|
||||
vi.clearAllMocks();
|
||||
mockDb.query.mockReset();
|
||||
|
||||
// Reset the withTransaction mock before each test
|
||||
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
||||
|
||||
@@ -47,6 +47,7 @@ describe('Budget DB Service', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockDb.query.mockReset();
|
||||
// Instantiate the repository with the minimal mock db for each test
|
||||
budgetRepo = new BudgetRepository(mockDb);
|
||||
});
|
||||
|
||||
@@ -28,6 +28,7 @@ import { logger as mockLogger } from '../logger.server';
|
||||
describe('Conversion DB Service', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockPoolInstance.query.mockReset();
|
||||
// Make getPool return our mock instance for each test
|
||||
vi.mocked(getPool).mockReturnValue(mockPoolInstance as any);
|
||||
});
|
||||
|
||||
@@ -46,6 +46,7 @@ describe('Flyer DB Service', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockPoolInstance.query.mockReset();
|
||||
//In a transaction, `pool.connect()` returns a client. That client has a `release` method.
|
||||
// For these tests, we simulate this by having `connect` resolve to the pool instance itself,
|
||||
// and we ensure the `release` method is mocked on that instance.
|
||||
@@ -586,18 +587,6 @@ describe('Flyer DB Service', () => {
|
||||
});
|
||||
|
||||
describe('getFlyers', () => {
|
||||
const expectedQuery = `
|
||||
SELECT
|
||||
f.*,
|
||||
json_build_object(
|
||||
'store_id', s.store_id,
|
||||
'name', s.name,
|
||||
'logo_url', s.logo_url
|
||||
) as store
|
||||
FROM public.flyers f
|
||||
JOIN public.stores s ON f.store_id = s.store_id
|
||||
ORDER BY f.created_at DESC LIMIT $1 OFFSET $2`;
|
||||
|
||||
it('should use default limit and offset when none are provided', async () => {
|
||||
console.log('[TEST DEBUG] Running test: getFlyers > should use default limit and offset');
|
||||
const mockFlyers: Flyer[] = [createMockFlyer({ flyer_id: 1 })];
|
||||
@@ -611,7 +600,7 @@ describe('Flyer DB Service', () => {
|
||||
);
|
||||
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(
|
||||
expectedQuery,
|
||||
expect.stringContaining('FROM public.flyers f'),
|
||||
[20, 0], // Default values
|
||||
);
|
||||
});
|
||||
@@ -629,7 +618,7 @@ describe('Flyer DB Service', () => {
|
||||
);
|
||||
|
||||
expect(mockPoolInstance.query).toHaveBeenCalledWith(
|
||||
expectedQuery,
|
||||
expect.stringContaining('FROM public.flyers f'),
|
||||
[10, 5], // Provided values
|
||||
);
|
||||
});
|
||||
|
||||
@@ -29,6 +29,7 @@ describe('Gamification DB Service', () => {
|
||||
beforeEach(() => {
|
||||
// Reset the global mock's call history before each test.
|
||||
vi.clearAllMocks();
|
||||
mockDb.query.mockReset();
|
||||
|
||||
// Instantiate the repository with the mock pool for each test
|
||||
gamificationRepo = new GamificationRepository(mockDb);
|
||||
|
||||
@@ -30,6 +30,7 @@ describe('Notification DB Service', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockPoolInstance.query.mockReset();
|
||||
// Instantiate the repository with the mock pool for each test
|
||||
|
||||
notificationRepo = new NotificationRepository(mockPoolInstance as unknown as Pool);
|
||||
|
||||
@@ -35,6 +35,7 @@ describe('Personalization DB Service', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockQuery.mockReset();
|
||||
// Reset the withTransaction mock before each test
|
||||
vi.mocked(withTransaction).mockImplementation(async (callback) => {
|
||||
const mockClient = { query: vi.fn() };
|
||||
|
||||
@@ -27,6 +27,7 @@ import { logger as mockLogger } from '../logger.server';
|
||||
describe('Price DB Service', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockPoolInstance.query.mockReset();
|
||||
// Make getPool return our mock instance for each test
|
||||
vi.mocked(getPool).mockReturnValue(mockPoolInstance as any);
|
||||
});
|
||||
|
||||
@@ -34,6 +34,7 @@ describe('Reaction DB Service', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockDb.query.mockReset();
|
||||
reactionRepo = new ReactionRepository(mockDb);
|
||||
});
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ describe('Recipe DB Service', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockQuery.mockReset();
|
||||
// Instantiate the repository with the mock pool for each test
|
||||
recipeRepo = new RecipeRepository(mockPoolInstance as unknown as Pool);
|
||||
});
|
||||
|
||||
@@ -36,6 +36,7 @@ describe('Shopping DB Service', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockPoolInstance.query.mockReset();
|
||||
// Instantiate the repository with the mock pool for each test
|
||||
shoppingRepo = new ShoppingRepository(mockPoolInstance as unknown as Pool);
|
||||
});
|
||||
|
||||
@@ -62,6 +62,7 @@ describe('User DB Service', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockPoolInstance.query.mockReset();
|
||||
userRepo = new UserRepository(mockPoolInstance as unknown as PoolClient);
|
||||
// Provide a default mock implementation for withTransaction for all tests.
|
||||
vi.mocked(withTransaction).mockImplementation(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// src/tests/integration/gamification.integration.test.ts
|
||||
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
||||
import { describe, it, expect, beforeAll, afterAll, vi, beforeEach } from 'vitest';
|
||||
import supertest from 'supertest';
|
||||
import path from 'path';
|
||||
import fs from 'node:fs/promises';
|
||||
@@ -70,8 +70,13 @@ describe('Gamification Flow Integration Test', () => {
|
||||
fullName: 'Gamification Tester',
|
||||
request,
|
||||
}));
|
||||
});
|
||||
|
||||
// Setup default mock response for the AI service's extractCoreDataFromFlyerImage method.
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Reset AI Service Mock to default success state
|
||||
mockExtractCoreData.mockReset();
|
||||
mockExtractCoreData.mockResolvedValue({
|
||||
store_name: 'Gamification Test Store',
|
||||
valid_from: null,
|
||||
@@ -87,6 +92,9 @@ describe('Gamification Flow Integration Test', () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Reset Image Processor Mock
|
||||
vi.mocked(imageProcessor.generateFlyerIcon).mockResolvedValue('mock-icon.webp');
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// src/tests/integration/recipe.integration.test.ts
|
||||
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
||||
import { describe, it, expect, beforeAll, afterAll, vi, afterEach } from 'vitest';
|
||||
import supertest from 'supertest';
|
||||
import { createAndLoginUser } from '../utils/testHelpers';
|
||||
import { cleanupDb } from '../utils/cleanup';
|
||||
@@ -49,6 +49,12 @@ describe('Recipe API Routes Integration Tests', () => {
|
||||
createdRecipeIds.push(testRecipe.recipe_id);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Reset the mock to its default state for the next test
|
||||
vi.mocked(aiService.generateRecipeSuggestion).mockResolvedValue('Default Mock Suggestion');
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
vi.unstubAllEnvs();
|
||||
// Clean up all created resources
|
||||
|
||||
Reference in New Issue
Block a user