true
================================================
FILE: CLAUDE.md
================================================
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Receipt Wrangler is a full-stack receipt management and splitting application with OCR-powered scanning, AI-assisted data extraction, and multi-user group management. This is a **monorepo** containing three main components:
- **api/** - Go backend service (port 8081)
- **desktop/** - Angular 19 web interface (port 4200 dev, port 80 production)
- **mobile/** - Flutter cross-platform mobile app
- **docker/** - Monolith Docker build configuration
Each component has its own CLAUDE.md with detailed component-specific guidance. This file covers monorepo-level architecture and workflows.
## Monorepo Architecture
### Component Communication
- **API Contract**: OpenAPI 3.1 specification in `api/swagger.yml` defines the API contract
- **Client Generation**: API clients are auto-generated from swagger.yml using `api/generate-client.sh`
- Desktop: TypeScript Angular client → `desktop/src/open-api/`
- Mobile: Dart Dio client → `mobile/api/`
- MCP: TypeScript client for MCP integration
- **Development Flow**: Changes to API → update swagger.yml → regenerate clients → update frontend
### Technology Stack
- **Backend**: Go 1.24 with Chi router, GORM ORM, Asynq background jobs
- **Frontend**: Angular 19 with NGXS state management, Material + Bootstrap UI
- **Mobile**: Flutter with Provider state management, go_router navigation
- **Infrastructure**: Docker, nginx, PostgreSQL/MySQL/SQLite
## Docker Deployment
### Production Build (Monolith)
The `docker/Dockerfile` builds a single container with both API and web interface:
- Stage 1: Build Angular desktop app
- Stage 2: Build Go API and install dependencies (Tesseract, ImageMagick, Python)
- Final: nginx serves frontend, proxies `/api` to Go backend on port 80
### Development Build
The `docker/dev/Dockerfile` includes:
- All production components plus development tools
- SSH access for debugging (port 22, password: "development")
- Documentation site build from receipt-wrangler-doc repo
- Java runtime for OpenAPI generator
### Build Commands
```bash
# Production monolith
docker build -f docker/Dockerfile -t receipt-wrangler .
# Development container
docker build -f docker/dev/Dockerfile -t receipt-wrangler-dev .
```
## API Client Regeneration
When the API swagger.yml changes, regenerate clients:
```bash
# From api/ directory
./generate-client.sh desktop ../desktop/src/open-api
./generate-client.sh mobile ../mobile/api
./generate-client.sh mcp
```
**IMPORTANT**: Never manually edit generated client code in `desktop/src/open-api/` or `mobile/api/`. Changes will be overwritten.
## Component Development
### Backend Development (api/)
```bash
cd api
go run main.go # Run API server
go test -v ./... # Run tests
./set-up-dependencies.sh # Install system deps (first time)
```
See `api/CLAUDE.md` for detailed backend architecture and testing requirements.
### Frontend Development (desktop/)
```bash
cd desktop
npm start # Dev server with API proxy (localhost:4200)
npm test # Run tests with coverage
npm run build # Production build
```
See `desktop/CLAUDE.md` for Angular architecture, NGXS state management, and component structure.
### Mobile Development (mobile/)
```bash
cd mobile
flutter run # Run on device/emulator
flutter test # Run tests
flutter build apk # Build Android APK
flutter build ios # Build iOS app
```
See `mobile/CLAUDE.md` for Flutter architecture, Provider state management, and navigation.
## Critical Cross-Component Considerations
### API Changes Workflow
1. Modify backend code in `api/internal/`
2. Update `api/swagger.yml` to reflect API changes
3. Regenerate clients: `cd api && ./generate-client.sh desktop ../desktop/src/open-api`
4. Update frontend code to use new client methods
5. Test integration between components
### Authentication Flow
- JWT-based authentication with refresh tokens
- Backend issues tokens in `api/internal/handlers/auth.go`
- Desktop stores tokens via NGXS persistent storage
- Mobile uses `flutter_secure_storage` for secure token storage
- All API endpoints except `/api/auth/login` and `/api/auth/signup` require authentication
### State Management Patterns
- **Backend**: Service layer handles business logic, repositories handle data access
- **Desktop**: NGXS store with actions/selectors, persistent storage for auth/preferences
- **Mobile**: Provider pattern with ChangeNotifier models, models own their state
### Background Processing
- Backend uses Asynq for async jobs (OCR processing, email polling, cleanup)
- Long-running operations (OCR, AI extraction) run as background jobs
- Frontend polls for completion or uses WebSocket-like patterns where implemented
## Version Management
Each component has version tagging scripts:
- `api/tag-version.sh` - Tag API version
- `desktop/tag-version.sh` - Tag desktop version
- `mobile/tag-version.sh` - Tag mobile version
Version is embedded in Docker builds via `VERSION` and `BUILD_DATE` build args.
## Data Persistence
### Development
- API defaults to SQLite in `api/sqlite/`
- Desktop proxy config in `desktop/proxy.conf.json` routes to localhost:8081
- Mobile configures API base URL in app settings
### Production (Docker)
- Volumes for persistent data:
- `/app/receipt-wrangler-api/data` - Receipt images and uploads
- `/app/receipt-wrangler-api/sqlite` - SQLite database
- `/app/receipt-wrangler-api/logs` - Application logs
- nginx serves frontend from `/usr/share/nginx/html`
- API runs on same container, proxied via nginx
## Common Pitfalls
1. **Forgot to regenerate clients**: After API changes, clients are out of sync → regenerate!
2. **Editing generated code**: Changes to `desktop/src/open-api/` or `mobile/api/` will be lost
3. **Missing system dependencies**: API requires Tesseract, ImageMagick → run `api/set-up-dependencies.sh`
4. **Test database cleanup**: Failed Go tests leave `app.db` in test dirs → remove before rerunning
5. **Port conflicts**: API (8081), desktop dev (4200), docker prod (80) must be available
6. **CORS in development**: Desktop proxy handles CORS, but mobile needs proper API base URL
## Project Structure Summary
```
receipt-wrangler-api/ # Monorepo root
├── api/ # Go backend
│ ├── internal/ # Core application code
│ │ ├── handlers/ # HTTP handlers
│ │ ├── services/ # Business logic
│ │ ├── repositories/ # Database access
│ │ ├── models/ # Data models
│ │ └── wranglerasynq/ # Background jobs
│ ├── swagger.yml # API specification (source of truth)
│ └── CLAUDE.md # Backend-specific guidance
├── desktop/ # Angular web app
│ ├── src/
│ │ ├── app/ # Application modules
│ │ ├── store/ # NGXS state management
│ │ ├── shared-ui/ # Reusable components
│ │ └── open-api/ # Generated API client (DO NOT EDIT)
│ └── CLAUDE.md # Frontend-specific guidance
├── mobile/ # Flutter mobile app
│ ├── lib/
│ │ ├── models/ # Provider state models
│ │ ├── groups/ # Group features
│ │ ├── receipts/ # Receipt features
│ │ └── shared/ # Shared widgets
│ ├── api/ # Generated API client (DO NOT EDIT)
│ └── CLAUDE.md # Mobile-specific guidance
└── docker/ # Docker build configs
├── Dockerfile # Production monolith
└── dev/Dockerfile # Development container
```
## Code Changes Philosophy
- Prefer minimal, targeted changes. Do not refactor or restructure code beyond what was explicitly requested.
- A primary focus of yours is overall code quality. Your focus should be on producing code that is stable, flexible when
needed, readable and maintainable. You should not be writing code that is difficult to read, confusing, insecure or
too long.
- Follow **DRY (Don't Repeat Yourself) pragmatically**. If two or more places share nearly identical logic that would
need to be updated together, extract it into a shared utility, function, or component. This is not a dogmatic rule —
three similar lines in a single file or minor template repetition is fine. Apply DRY when it meaningfully reduces
maintenance burden, not for every tiny duplication.
- When the first approach fails, stop and ask the user for direction rather than trying multiple speculative approaches
in sequence.
- After you have completed the planning phase, and you have your plan, please iterate over your plan at a maximum of 3
times. During these iterations, your goals are to verify that your code makes sense, and solves the requested things,
that your code is sound, secure and consistent with style across the codebase, and that your code is clean, and not a
hacked together solution.
## Parallel Agent Execution
When a task spans multiple components (e.g., backend `api/` and frontend `desktop/` or `mobile/`), follow these rules:
- **Run backend and frontend agents in parallel** whenever possible. Do not serialize work across components unless
there is a hard dependency.
- **Frontend agents should order their work to defer backend-dependent tasks.** If the frontend needs something from the
backend (generated client, models, API endpoints), schedule that work last so independent frontend work happens first.
- **If the frontend agent is blocked on the backend agent** (e.g., waiting for a generated client, new API models, or
endpoint changes), the frontend agent should:
1. Continue planning its backend-dependent work (design the component, write the template, stub the types).
2. **Wait** for the backend agent to finish before executing backend-dependent code. Do not guess at API shapes or
generate placeholder clients.
3. Resume execution once the backend deliverables are available.
- **The backend agent should signal completion clearly** — after finishing its work, the orchestrating agent should
trigger any required client regeneration (e.g., `./generate-client.sh desktop ../desktop/src/open-api`) before
unblocking the frontend agent.
- **Mobile (`mobile/`) changes** follow the same pattern: if a backend change requires a mobile update, run the mobile
agent in parallel with the desktop agent after the backend agent completes.
### Example Task Ordering
For a feature that adds a new API endpoint and a corresponding UI:
1. **Phase 1 (parallel):**
- Backend agent: handler → service → repository → route → tests → swagger update
- Frontend agent: independent UI work (layout, styling, routing, non-API components)
2. **Phase 2 (sequential, after backend completes):**
- Regenerate client (`cd api && ./generate-client.sh desktop ../desktop/src/open-api`)
- Frontend agent: wire up API calls, integrate generated types, write dependent components
3. **Phase 3 (parallel):**
- Backend agent: any follow-up fixes
- Frontend agent: integration tests, final UI polish
## Testing
- After ANY code change, run the full relevant test suite before considering the task complete.
- When tests fail, fix both the code AND the tests — don't assume tests are correct or code is correct without
verifying.
## Workflow Rules
- Always complete implementation AND verify (build + tests pass) before committing. Do not commit code that hasn't been
validated.
- During your planning sessions, explicitly check if your planned code introduces regressions. We want to make sure that
we do not break existing code, especially things that may not show themselves through build errors like scss changes,
conflicting styles, and so on.
- During your planning sessions, take a moment to think if there are any edge cases, or possible regressions or any
additional things for the user to test before considering the task complete.
- After implementing any full feature, always commit/push.
## CLAUDE.md Maintenance
- After modifying files in any component, check whether the corresponding `CLAUDE.md` needs updating.
- Each component has its own documentation: `api/CLAUDE.md`, `desktop/CLAUDE.md`, `mobile/CLAUDE.md`.
- If a change alters behavior, configuration, architecture, commands, or conventions documented in a `CLAUDE.md` file,
update that file to stay accurate before considering the task complete.
================================================
FILE: README.md
================================================
# Welcome to Receipt Wrangler
This repository contains the Receipt Wrangler API, Desktop app and Mobile app.
Previously these were separate repositories, they are now merged into one monolith repository for simplicity.
To get started, check out the directories containing each application.
================================================
FILE: desktop/.dockerignore
================================================
node_modules
.git
.gitignore
.angular
================================================
FILE: desktop/.editorconfig
================================================
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false
================================================
FILE: desktop/.gitignore
================================================
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
/.jest-cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# Playwright
/test-results
/playwright-report
/playwright/.cache
/e2e/.auth
# System files
.DS_Store
Thumbs.db
.npmrc
.qemu-arm-static
================================================
FILE: desktop/.vscode/extensions.json
================================================
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
"recommendations": ["angular.ng-template"]
}
================================================
FILE: desktop/.vscode/launch.json
================================================
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ng serve",
"type": "pwa-chrome",
"request": "launch",
"preLaunchTask": "npm: start",
"url": "http://localhost:4200/"
},
{
"name": "ng test",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: test",
"url": "http://localhost:9876/debug.html"
}
]
}
================================================
FILE: desktop/.vscode/tasks.json
================================================
{
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
},
{
"type": "npm",
"script": "test",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
}
]
}
================================================
FILE: desktop/CLAUDE.md
================================================
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Development Commands
### Core Development
- `npm start` - Start development server with proxy configuration (serves on localhost:4200, proxies /api to localhost:8081)
- `npm run build` - Build production application
- `npm run watch` - Build in watch mode for development
- `npm test` - Run unit tests with coverage
- `npm test:ci` - Run tests in CI mode with ChromeHeadless
- `npm run e2e` - Run Playwright end-to-end tests (see **E2E Testing** below)
- `npm run e2e:ui` - Run Playwright tests in interactive UI mode
- `npm run e2e:install` - Install Playwright browser binaries (one-time setup)
### Build Configuration
- Production builds go to `dist/receipt-wrangler/`
- Development server uses proxy configuration in `proxy.conf.json` to route API calls to backend
- Angular CLI configuration in `angular.json`
## Code Architecture
### Application Structure
Receipt Wrangler Desktop is an Angular 19 application with modular architecture using:
- **State Management**: NGXS store with persistent storage for application state
- **API Layer**: Auto-generated OpenAPI client in `src/open-api/` (do not manually edit these files)
- **Component Architecture**: Feature modules with lazy-loaded routing
- **UI Framework**: Angular Material + Bootstrap 5 + custom shared components
### Key Architectural Patterns
#### Module Organization
- Feature modules (receipts, dashboard, groups, etc.) with their own routing
- Shared UI components in `src/shared-ui/` for reusable elements
- Lazy-loaded modules for performance optimization
- Centralized store management with NGXS states
#### State Management (NGXS)
- All application state managed through NGXS store
- State persistence configured for key data (auth, user preferences, table states)
- Individual state files for each feature (receipt-table.state.ts, group.state.ts, etc.)
- Actions and state updates follow NGXS patterns
#### Component Structure
- Feature components organized by domain (receipts/, dashboard/, groups/)
- Shared UI components provide consistent design patterns
- Form components use reactive forms with custom validation
- Table components use base table service pattern for pagination and filtering
### Key Directories
#### Core Application
- `src/app/` - Main application module and routing
- `src/store/` - NGXS state management (18+ state files)
- `src/services/` - Application services and business logic
- `src/guards/` - Route guards for authentication and authorization
#### Features
- `src/receipts/` - Receipt management (forms, tables, processing)
- `src/dashboard/` - Customizable dashboard widgets and views
- `src/groups/` - Group management and member administration
- `src/categories/` and `src/tags/` - Receipt organization features
- `src/auth/` - Authentication and user management
#### Shared Infrastructure
- `src/shared-ui/` - 30+ reusable UI components (buttons, forms, tables, dialogs)
- `src/pipes/` - Custom Angular pipes for data transformation
- `src/utils/` - Utility functions and helpers
- `src/open-api/` - Generated API client (auto-generated, do not edit)
### Testing Strategy
- Unit tests use Jasmine/Karma framework
- Code coverage reporting with minimum thresholds
- Tests exclude auto-generated API code (`src/open-api/`)
- CI tests run in headless Chrome
### Development Environment
- Angular CLI 21 with TypeScript 5.9
- Bootstrap 5 + Angular Material for UI components
- NGXS for state management with Redux DevTools integration
- Strict TypeScript configuration with comprehensive compiler options
### API Integration
- Backend API proxied through development server
- OpenAPI client generated from backend specification
- API base path configurable through environment
- HTTP interceptors handle authentication and error responses
### Code Conventions
- SCSS for styling with component-scoped styles
- TypeScript strict mode enabled
- Angular style guide followed for component organization
- Lazy loading for feature modules to optimize bundle size
## Signals & Zoneless Change Detection
This application uses Angular's signal-based reactivity model with zoneless change detection (`provideZonelessChangeDetection()`). All new code MUST follow these patterns.
### Signal Primitives — Decision Guide
| Need | Use | NOT |
|------|-----|-----|
| Mutable state | `signal()` | Plain class properties |
| Read-only derived value | `computed()` | `effect()` that copies signals |
| Writable derived state (resets on dependency change, can be overridden) | `linkedSignal()` | `effect()` that sets a signal |
| Sync signal state to imperative/external APIs (DOM, localStorage, canvas, analytics) | `effect()` | — |
| DOM measurement/manipulation after render | `afterRenderEffect()` | `effect()` + `setTimeout` |
| Async data fetching | `resource()` | Manual subscribe + signal set |
| Observable → Signal bridge | `toSignal()` | `subscribe()` + signal set |
| Signal → Observable bridge | `toObservable()` | — |
### signal() — Writable State
- Use for mutable, source-of-truth state in components or services.
- Prefer `signal()` over plain class properties — signals automatically notify Angular's change detection.
- Provide a custom equality function when needed to avoid unnecessary updates.
```typescript
count = signal(0);
items = signal([]);
```
### computed() — Derived State
- Use whenever a value is derived from other signals. Always prefer over `effect()` for derivations.
- Computed signals are lazy (not evaluated until read) and cached (not recalculated until dependencies change).
- Safe to perform expensive operations (e.g., filtering arrays) inside computed.
```typescript
fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
filteredItems = computed(() => this.items().filter(i => i.active));
```
### linkedSignal() — Writable Derived State
- Use when a value normally follows a computation but can be manually overridden.
- Resets to the computed value when dependencies change, but allows `set()`/`update()`.
- Perfect for selections that reset when options change.
```typescript
// Resets to first option when options change, but user can select manually
selectedOption = linkedSignal(() => this.options()[0]);
```
### effect() — Side Effects (Last Resort)
- **NEVER** use `effect()` to derive state or copy signal values between signals. Use `computed()` or `linkedSignal()` instead.
- **ONLY** use for syncing to non-reactive/imperative APIs: logging, localStorage, canvas rendering, third-party UI libraries.
- Effects run during change detection. They do not need `allowSignalWrites` (removed in Angular 19).
- Use `afterRenderEffect()` instead when you need to read DOM properties (offsetWidth, etc.) after rendering.
```typescript
// GOOD: Syncing to localStorage
effect(() => {
localStorage.setItem('theme', this.theme());
});
// BAD: Deriving state — use computed() instead
effect(() => {
this.fullName.set(`${this.firstName()} ${this.lastName()}`); // ❌ NEVER DO THIS
});
```
### Signal Inputs — input() and input.required()
- Use `input()` for optional inputs with defaults. Use `input.required()` for required inputs.
- Signal inputs are read-only (`InputSignal`). Template binding syntax `[prop]="value"` is unchanged.
- Use `computed()` to derive values from inputs. Use `effect()` only for imperative side effects triggered by input changes.
- Use `model()` for two-way binding (component modifies a value based on user interaction, e.g., custom form controls).
```typescript
// Required input — no undefined in type
mode = input.required();
// Optional input with default
disabled = input(false);
// Optional input without default
tooltip = input();
// Two-way binding
value = model('');
// Deriving from inputs — use computed, NOT effect
displayText = computed(() => this.mode() === FormMode.Edit ? 'Save' : 'Create');
```
**Replacing ngOnChanges:** Convert input-watching logic from `ngOnChanges` to `computed()` (for derived values) or `effect()` (for imperative side effects like loading data).
```typescript
// Before (ngOnChanges)
ngOnChanges(changes: SimpleChanges) {
if (changes['groupId']) this.loadData();
}
// After (effect for imperative side effect)
constructor() {
effect(() => {
const id = this.groupId();
if (id) this.loadData(id);
});
}
```
### Signal Outputs — output()
- Use `output()` instead of `@Output() + EventEmitter`. Template syntax `(event)="handler($event)"` is unchanged.
- Use `outputFromObservable()` when the source is an Observable.
```typescript
clicked = output();
// Emit: this.clicked.emit(event);
```
### Signal Queries — viewChild() / viewChildren()
- Use `viewChild()` / `viewChildren()` instead of `@ViewChild` / `@ViewChildren`.
- Access via signal call: `this.paginator()` instead of `this.paginator`.
- Use `viewChild.required()` when the element is guaranteed to exist (not behind `@if`).
```typescript
paginator = viewChild.required(MatPaginator);
optionalEl = viewChild('myEl');
items = viewChildren(ItemComponent);
```
### RxJS Interop
- **`toSignal(observable)`**: Converts Observable to Signal. Creates a subscription — call once and reuse the signal, never call repeatedly. Automatically unsubscribes on destroy.
- Provide `initialValue` for Observables that don't emit synchronously.
- Use `requireSync: true` for BehaviorSubject or other synchronous sources.
- **`toObservable(signal)`**: Converts Signal to Observable. Only emits the latest stabilized value.
- **`takeUntilDestroyed()`**: Replaces `@UntilDestroy()` / `untilDestroyed(this)`. Use in constructor or pass `DestroyRef`.
- **`outputFromObservable()`**: Declares an output from an Observable source.
```typescript
// NGXS selector → signal (preferred pattern)
groups = this.store.selectSignal(GroupState.groups);
// HTTP Observable → signal
data = toSignal(this.http.get('/api/data'), { initialValue: [] });
// Cleanup subscriptions
constructor() {
this.someObservable$.pipe(
takeUntilDestroyed(),
).subscribe(val => this.doSomething(val));
}
```
### NGXS State Access
- Use `store.selectSignal()` instead of `@Select` decorator for template-bound state. Returns a `Signal`.
- `store.selectSnapshot()` remains valid for synchronous one-time reads in methods.
- Remove `| async` pipe from templates — use signal reads `()` instead.
```typescript
// Before
@Select(AuthState.isLoggedIn) isLoggedIn!: Observable;
// Template: *ngIf="isLoggedIn | async"
// After
isLoggedIn = this.store.selectSignal(AuthState.isLoggedIn);
// Template: @if (isLoggedIn()) { ... }
```
### Zoneless Change Detection Rules
Angular no longer uses zone.js. Change detection is triggered ONLY by:
1. **Signal writes** — `signal.set()`, `signal.update()`, `computed()` recalculation
2. **`ChangeDetectorRef.markForCheck()`** — for non-signal reactive patterns (AsyncPipe calls this automatically)
3. **Template event bindings** — `(click)="handler()"` automatically triggers CD
4. **`ComponentRef.setInput()`** — programmatic input setting
**Key implications:**
- Plain property mutations (`this.foo = 'bar'`) in async callbacks (subscribe, setTimeout, Promise.then) will NOT trigger change detection. Always use signals for state that affects templates.
- `ChangeDetectorRef.detectChanges()` still works but is rarely needed — prefer signals.
- `setTimeout` still works for delays but won't auto-trigger CD. The callback must write to a signal if the template needs updating.
- All `@HostListener` handlers automatically trigger CD (same as template events).
### Testing with Zoneless
- Add `provideZonelessChangeDetection()` to `TestBed.configureTestingModule` providers.
- Prefer `await fixture.whenStable()` over `fixture.detectChanges()` for most realistic test behavior.
- Use `TestBed.flushEffects()` when testing effect-based logic.
## E2E Testing
End-to-end tests live in `e2e/` and use **Playwright**. They drive the real Angular UI against a real Go API. Config is `playwright.config.ts`.
### Running locally
1. **One-time:** install browsers — `npm run e2e:install`.
2. **One-time:** sign up the two e2e accounts against your local DB. The **first** signup is auto-promoted to admin, so order matters. With the API running, go to `http://localhost:4200/auth/sign-up` and create:
- Admin first: username `e2e-admin`, password `e2e-admin-password`
- Then user: username `e2e-user`, password `e2e-user-password`
3. **Every run:** source the dev env script so the `E2E_*` vars are exported:
```bash
cd ../api/dev && source switch-to-sqlite.sh && cd -
```
(`switch-to-mariadb.sh` / `switch-to-postgresql.sh` work the same — all three export the same `E2E_*` defaults.)
4. Start the Go API separately (`cd ../api && go run main.go`). Playwright auto-starts the Angular dev server via its `webServer` config, but it cannot launch the API.
5. Run the tests: `npm run e2e` (or `npm run e2e:ui` for watch-style debugging).
### CI
In CI the same spec files run against the demo URL. GitHub secrets populate the `E2E_*` vars — point `E2E_BASE_URL` at `https://demo.receiptwrangler.io` and supply the secret credentials. When `E2E_BASE_URL` is remote, the config skips the `webServer` block and does not start a local dev server.
### Best practices (follow these when adding new e2e tests)
**Locators — use user-facing, auto-retrying selectors.**
- Prefer `page.getByRole('button', { name: 'Login' })`, `page.getByLabel('Password')`, `page.getByPlaceholder(...)`, `page.getByText(...)`.
- Use `page.getByTestId(...)` only when no accessible role/label exists. The codebase has no `data-testid` convention yet — add one on a component only when role/label truly can't identify the element.
- Avoid raw CSS/XPath (`page.locator('.btn-primary')`) — brittle to refactors.
**Assertions — rely on web-first expects, never `waitForTimeout`.**
- Use `await expect(locator).toBeVisible()`, `toHaveText()`, `toHaveURL()`, `toHaveCount()` — they auto-retry until `expect.timeout`.
- Never `await page.waitForTimeout(ms)` — it's a fixed sleep and flakes.
- Prefer `await page.waitForURL(/.../)` or `await page.waitForResponse(...)` for navigation/network waits.
**Isolation — each test gets a fresh `BrowserContext`.**
- No cookies/localStorage/session leak between siblings.
- Do NOT hand-write state-sharing between tests. If two tests need a logged-in session, use Playwright's `storageState` pattern (see below), not module-level globals.
**Auth — reuse login state, don't re-login in every test.**
- Current suite is tiny (login IS the test), so each test logs in via the UI. Fine for now.
- When the suite grows, switch to the **setup project** pattern: a `*.setup.ts` file logs in once and saves `storageState` to `e2e/.auth/.json`; other tests declare `test.use({ storageState: 'e2e/.auth/user.json' })`. Keep `.auth/` git-ignored — it contains session cookies.
- One storageState file per role (admin, user). Never share one login across roles.
**`webServer` — for processes Playwright can launch.**
- The config uses `webServer` to start `npm start` when `E2E_BASE_URL` is localhost, and skips it when the URL is remote. `reuseExistingServer: !process.env.CI` lets local devs keep `ng serve` running between runs.
- Playwright cannot launch the Go API — that's always a separate process.
**Env vars and secrets.**
- Read via `process.env.E2E_*` — never hardcode credentials.
- Local defaults come from `api/dev/switch-to-*.sh`. CI values come from GitHub secrets.
- Never commit `.env` files or `e2e/.auth/` artifacts.
**Parallelism and flake budget.**
- `fullyParallel: true` is on. Tests must not mutate shared server state in ways that collide (same DB row, same uploaded file, same group membership). When you need mutation, create unique data per test (timestamp/UUID in names) and clean up after.
- `retries: 2` in CI, `0` locally — a test that only passes with retries is a bug, not a feature. Fix the root cause.
- `trace: 'on-first-retry'` captures a trace file on the first retry; view with `npx playwright show-trace `. Do not set `trace: 'on'` — too heavy.
**Writing selectors for this app.**
- Forms use a custom `` wrapper over ``. `page.getByLabel('Username')` resolves through the `` association.
- Submit buttons use `` rendering `