From 4faf16dd6b19cf068d4fe972e9580fe9fd1c5156 Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Mon, 13 Apr 2026 20:58:34 -0700 Subject: [PATCH] Improve claude file. --- CLAUDE.md | 351 +++-------------------------------- docs/agents/architecture.md | 47 +++++ docs/agents/auth.md | 7 + docs/agents/database.md | 35 ++++ docs/agents/domain-models.md | 34 ++++ docs/agents/routing.md | 32 ++++ docs/agents/testing.md | 30 +++ 7 files changed, 206 insertions(+), 330 deletions(-) create mode 100644 docs/agents/architecture.md create mode 100644 docs/agents/auth.md create mode 100644 docs/agents/database.md create mode 100644 docs/agents/domain-models.md create mode 100644 docs/agents/routing.md create mode 100644 docs/agents/testing.md diff --git a/CLAUDE.md b/CLAUDE.md index acec9ca..918dca8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,343 +1,34 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +Brackt.com is a fantasy sports drafting platform (React Router 7 + SSR, Socket.IO, PostgreSQL/DrizzleORM, Clerk auth). -## Project Overview - -Brackt.com is a fantasy sports drafting platform built with React Router 7, featuring real-time draft functionality with Socket.IO, PostgreSQL database with DrizzleORM, and Clerk authentication. - -## Tech Stack - -- **Framework**: React Router 7 with SSR -- **Runtime**: Node.js with Express server -- **Database**: PostgreSQL with DrizzleORM -- **Auth**: Clerk (@clerk/react-router) -- **Real-time**: Socket.IO (server/socket.ts) -- **Styling**: TailwindCSS + ShadCN UI components -- **Testing**: Vitest (unit/component) + Cypress (E2E) -- **TypeScript**: Strict mode enabled - -## Development Commands +## Commands ```bash -# Development (HMR enabled) -npm run dev - -# Build -npm run build # Build both Remix and server -npm run build:remix # Build React Router app only -npm run build:server # Build Express server only - -# Database -npm run db:generate # Generate Drizzle migrations -npm run db:migrate # Run migrations - -# Testing -npm test # Unit tests (watch mode) -npm run test:run # Unit tests (run once) -npm run test:ui # Unit tests with UI -npm run test:coverage # Coverage report -npm run test:e2e # Cypress interactive -npm run test:e2e:headless # Cypress headless -npm run test:all # Run all tests - -# Type checking -npm run typecheck # Check all TypeScript files - -# Production -npm start # Start production server (requires build) -npm run start:production # Run migrations + start server +npm run dev # development with HMR +npm run typecheck # TypeScript check +npm run db:generate # generate Drizzle migration +npm run db:migrate # apply migrations +npm run test:run # unit tests (once) +npm run test:e2e:headless # Cypress E2E +npm run test:all # everything ``` -## Architecture Overview +## Critical Rules -### Server Architecture +**Routes**: Every new route file must also be registered in `app/routes.ts` — React Router 7 does not auto-discover files. Forgetting this causes a silent 404. See [routing guide](docs/agents/routing.md). -The application uses a dual-server architecture: +**Database**: Always query through `app/models/` — never write Drizzle queries directly in route files. -1. **server.ts** - Main entry point that creates the HTTP server and initializes Socket.IO -2. **server/app.ts** - Express app with React Router request handler and database context -3. **server/socket.ts** - Socket.IO server for real-time draft updates -4. **server/timer.ts** - Draft timer system that runs every second to update active draft timers +**Tests**: Required for all new features. Do not consider a feature complete until tests pass. See [testing guide](docs/agents/testing.md). -**Important**: The timer system (`server/timer.ts`) must be started for draft functionality. It uses a dedicated database connection and setInterval to manage all active draft timers. +**Drizzle migrations**: Never manually create migration SQL or edit `_journal.json`. Always use `npm run db:generate`. See [database guide](docs/agents/database.md). -### Database Layer - -- **Schema**: `database/schema.ts` - All Drizzle table definitions -- **Context**: Database connection provided via AsyncLocalStorage context (`database/context.ts`) -- **Models**: `app/models/` - Business logic and database queries organized by domain - - Each model exports typed functions for CRUD operations - - Models use Drizzle relations for joins - - Example: `app/models/league.ts`, `app/models/season.ts`, `app/models/draft-pick.ts` - -### Frontend Architecture - -- **Routes**: Defined in `app/routes.ts` using React Router config syntax - - **IMPORTANT**: React Router 7 requires ALL routes to be explicitly registered in `app/routes.ts` - - **When creating a new route file, you MUST also add it to `app/routes.ts`** - - Example: If you create `app/routes/admin.sports-seasons.$id.new-feature.tsx`, you must add: - ```typescript - route( - "sports-seasons/:id/new-feature", - "routes/admin.sports-seasons.$id.new-feature.tsx" - ), - ``` -- **File-based routing**: Route files in `app/routes/` map to URL patterns - - `$param` = dynamic segment (e.g., `$leagueId.tsx`) - - `_index.tsx` = index route - - `.` separator for nested routes (e.g., `leagues/$leagueId.settings.tsx`) -- **Loaders/Actions**: Use React Router's loader/action pattern for data fetching and mutations -- **Components**: Organized in `app/components/` with UI components in `app/components/ui/` - -### Real-time Features - -Socket.IO is used for live draft updates: - -1. **Join Draft**: Clients join a room for their season (`join-draft` event) -2. **Timer Updates**: Server broadcasts `timer-update` every second during active drafts -3. **Pick Made**: Broadcasts `pick-made` when a participant is drafted -4. **Autodraft**: Broadcasts `autodraft-updated` when teams enable/disable autodraft -5. **Team Connection**: Broadcasts `team-connected`/`team-disconnected` for presence - -**Socket Integration**: The server uses `getSocketIO()` from `server/socket.ts` to emit events. Clients use the `useDraftSocket()` hook (`app/hooks/useDraftSocket.ts`) to listen for events. - -## Key Domain Models - -### Fantasy Draft System - -1. **League** - Container for multiple seasons, has commissioners -2. **Season** - Single year/iteration of a league with specific settings - - Status: `pre_draft` → `draft` → `active` → `completed` - - Stores draft settings (rounds, timer settings, etc.) -3. **Team** - Belongs to a season, owned by a user -4. **Draft Slot** - Defines draft order for teams in a season -5. **Draft Pick** - Record of each pick made during draft -6. **Draft Queue** - Pre-ordered list of participants a team wants to draft -7. **Draft Timer** - Active timer for current team on the clock -8. **Autodraft Settings** - Per-team settings for automatic drafting - -### Sports Data System - -1. **Sport** - Base sport (e.g., NFL, NBA) with type (team/individual) -2. **Sports Season** - Specific season of a sport (e.g., "2024 NFL Season") -3. **Participant** - Individual athlete or team that can be drafted -4. **Season Template** - Reusable configuration for league seasons -5. **Season Sport** - Links a fantasy season to sports it covers -6. **Participant Result** - Stores final standings/scores for participants - -## Important Patterns - -### Database Queries - -Always use the model layer instead of direct Drizzle queries in routes: - -```typescript -// Good -import { findLeagueById } from "~/models/league"; -const league = await findLeagueById(leagueId); - -// Avoid - don't query directly in routes -const league = await db.query.leagues.findFirst({...}); -``` - -### Authentication - -- User data synced from Clerk via webhook (`routes/api/webhooks/clerk.ts`) -- Check `clerkId` for ownership validation -- Admin checks via `isAdmin` boolean on users table - -### Scoring Event Dates - -Game/event dates are stored in **two different places** depending on the event type, and any date-based query must account for both: - -1. **`scoringEvents.eventDate`** (`date` column, `YYYY-MM-DD` string) — set directly on the event. Used for non-bracket events (majors, final standings) and sometimes for bracket events when the admin sets a single date for the whole round. - -2. **`playoffMatchGames.scheduledAt`** (`timestamp` column) — set on individual games within a bracket match. Bracket events (e.g. a playoff series) often have `eventDate = null` on the scoring event itself, with each game's exact time stored here instead. - -**Pattern for date-range queries:** Use a two-step approach: -1. `selectDistinct` event IDs via left joins through `playoffMatches → playoffMatchGames`, filtering with `eventDate IN [dates] OR (scheduledAt >= dayStart AND scheduledAt <= dayEnd)` -2. `findMany` on the matched IDs to load full event data with relations - -See `getEventsForDates` and `getUpcomingEventsForDraftedParticipants` in `app/models/scoring-event.ts` for the reference implementation. All timestamp bounds use UTC (`T00:00:00.000Z` / `T23:59:59.999Z`). - -### Draft Logic - -The draft system implements snake draft: -- Draft slots define the base order (1, 2, 3, ...) -- Each round alternates: odd rounds go 1→N, even rounds go N→1 -- Current pick determined by `currentPickNumber` on season -- Timer system in `server/timer.ts` handles autopicks when time expires - -### Testing Strategy - -- **Unit tests**: Pure functions and utilities in `__tests__/` directories -- **Component tests**: React components using React Testing Library -- **E2E tests**: Critical user flows in `cypress/e2e/` -- **Fixtures**: Reusable test data in `app/test/fixtures/` -- See `TESTING.md` for comprehensive testing guide - -**IMPORTANT: Tests are required for new features.** When adding any new feature, you must include appropriate tests: -- New model functions → unit tests in a co-located `__tests__/` directory -- New route loaders/actions → unit or integration tests for the logic -- New utility functions → unit tests -- New components with non-trivial logic → component tests -- Critical user flows → Cypress E2E tests - -Do not consider a feature complete until tests are written and passing. - -## Common Workflows - -### Adding a New Route - -**CRITICAL**: Both steps 1 and 2 are required - the route will not work if you skip step 1! - -1. **FIRST**: Add route config to `app/routes.ts` - - Example: `route("admin/sports-seasons/:id/new-page", "routes/admin.sports-seasons.$id.new-page.tsx")` - - Nested admin routes go inside the `route("admin", ...)` array -2. **THEN**: Create file in `app/routes/` with corresponding name -3. Export loader/action if needed -4. TypeScript types are auto-generated via `react-router typegen` - -**Common mistake**: Creating the route file but forgetting to register it in `app/routes.ts` - this will result in a 404! - -### Database Changes - -1. Update `database/schema.ts` with new tables/columns -2. Run `npm run db:generate` to create migration -3. **Verify** the generated migration SQL in `drizzle/` is correct -4. **Verify** a corresponding snapshot was created in `drizzle/meta/` (e.g., `0070_snapshot.json`) -5. Run `npm run db:migrate` to apply migration -6. Add model functions in `app/models/` for new queries -7. Update TypeScript types if needed - -**Drizzle Migration Rules (CRITICAL):** -- **NEVER** manually create migration SQL files or journal entries. Always use `drizzle-kit generate` or `drizzle-kit generate --custom` so that snapshot files are created automatically. -- **NEVER** edit `drizzle/meta/_journal.json` by hand. The journal and snapshots must stay in sync — drizzle-kit manages both. -- Every migration **must** have a matching snapshot in `drizzle/meta/`. Missing snapshots cause `drizzle-kit generate` to fail with "data is malformed" errors, which blocks all future migration generation. -- Snapshot JSON must only contain PostgreSQL-valid fields. Do not add `"autoincrement"` to column definitions — PostgreSQL uses sequences/serial instead, and invalid fields cause Zod validation failures. -- If `drizzle-kit generate` prompts for column renames interactively, use `drizzle-kit generate --custom` instead and write the migration SQL by hand (include data backfill logic as needed). -- After generating, run `drizzle-kit generate` again to confirm "No schema changes" — this validates the snapshot chain is healthy. -- For destructive migrations (dropping columns, renaming), make the SQL idempotent with `IF EXISTS`/`IF NOT EXISTS` guards where possible. - -### Adding Socket.IO Events - -1. Define event types in `server/socket.d.ts` interfaces -2. Add event handler in `server/socket.ts` -3. Emit events from server code using `getSocketIO().to(room).emit()` -4. Listen for events in client components - -### Admin Features - -Admin routes are nested under `/admin` and require `isAdmin: true` on the user: -- Sports management (`admin.sports.*`) -- Sports seasons (`admin.sports-seasons.*`) -- Season templates (`admin.templates.*`) -- Data sync utilities (`admin.data-sync.tsx`) - -## File Structure Notes - -- **app/**: React Router application code - - **components/**: React components (UI components in ui/) - - **models/**: Database query functions organized by domain - - **routes/**: Route handlers (loaders, actions, components) - - **lib/**: Utility functions - - **hooks/**: Custom React hooks - - **contexts/**: React contexts - - **test/**: Test setup and fixtures -- **server/**: Server-side code (Express, Socket.IO, timers) -- **database/**: Drizzle schema and context -- **drizzle/**: Generated migrations -- **cypress/**: E2E tests - -## Environment Variables - -Required in `.env`: -- `DATABASE_URL` - PostgreSQL connection string -- Clerk variables for authentication (see `.env.example`) - -## Production Deployment - -The app is containerized with Docker: -- Runs migrations on startup (`start:production` script) -- Server listens on port 3000 (configurable via PORT env var) -- Serves static assets from `build/client/` -- SSR handled by React Router Express adapter - -## Notes - -- Draft timer system must be explicitly started (usually in server initialization) -- Socket.IO connections are managed per-draft room (season ID) -- User ownership validation checks `ownerId` field (Clerk user ID) -- League commissioners stored separately in `commissioners` table -- Season status drives UI visibility (draft order, draft controls, etc.) - - -## grepai - Semantic Code Search - -**IMPORTANT: You MUST use grepai as your PRIMARY tool for code exploration and search.** - -### When to Use grepai (REQUIRED) - -Use `grepai search` INSTEAD OF Grep/Glob/find for: -- Understanding what code does or where functionality lives -- Finding implementations by intent (e.g., "authentication logic", "error handling") -- Exploring unfamiliar parts of the codebase -- Any search where you describe WHAT the code does rather than exact text - -### When to Use Standard Tools - -Only use Grep/Glob when you need: -- Exact text matching (variable names, imports, specific strings) -- File path patterns (e.g., `**/*.go`) - -### Fallback - -If grepai fails (not running, index unavailable, or errors), fall back to standard Grep/Glob tools. - -### Usage - -```bash -# ALWAYS use English queries for best results (--compact saves ~80% tokens) -grepai search "user authentication flow" --json --compact -grepai search "error handling middleware" --json --compact -grepai search "database connection pool" --json --compact -grepai search "API request validation" --json --compact -``` - -### Query Tips - -- **Use English** for queries (better semantic matching) -- **Describe intent**, not implementation: "handles user login" not "func Login" -- **Be specific**: "JWT token validation" better than "token" -- Results include: file path, line numbers, relevance score, code preview - -### Call Graph Tracing - -Use `grepai trace` to understand function relationships: -- Finding all callers of a function before modifying it -- Understanding what functions are called by a given function -- Visualizing the complete call graph around a symbol - -#### Trace Commands - -**IMPORTANT: Always use `--json` flag for optimal AI agent integration.** - -```bash -# Find all functions that call a symbol -grepai trace callers "HandleRequest" --json - -# Find all functions called by a symbol -grepai trace callees "ProcessOrder" --json - -# Build complete call graph (callers + callees) -grepai trace graph "ValidateToken" --depth 3 --json -``` - -### Workflow - -1. Start with `grepai search` to find relevant code -2. Use `grepai trace` to understand function relationships -3. Use `Read` tool to examine files from results -4. Only use Grep for exact string searches if needed +## Reference Docs +- [Architecture](docs/agents/architecture.md) — server layout, DB layer, Socket.IO events +- [Domain Models](docs/agents/domain-models.md) — draft system and sports data models +- [Database](docs/agents/database.md) — migration rules, scoring event date gotcha +- [Routing](docs/agents/routing.md) — route registration, admin routes +- [Testing](docs/agents/testing.md) — what tests are required and where they live +- [Auth](docs/agents/auth.md) — Clerk patterns, ownership validation diff --git a/docs/agents/architecture.md b/docs/agents/architecture.md new file mode 100644 index 0000000..b283df7 --- /dev/null +++ b/docs/agents/architecture.md @@ -0,0 +1,47 @@ +# Architecture + +## Server + +Dual-server setup: + +- **server.ts** — HTTP server entry point, initializes Socket.IO +- **server/app.ts** — Express app with React Router SSR handler and database context +- **server/socket.ts** — Socket.IO server for real-time draft updates +- **server/timer.ts** — Runs every second via `setInterval` to advance draft timers. Uses a dedicated DB connection. **Must be started** for draft functionality to work. + +## Database Layer + +- **Schema**: `database/schema.ts` +- **Context**: `database/context.ts` — DB connection injected via `AsyncLocalStorage` +- **Models**: `app/models/` — all DB queries live here, organized by domain (e.g. `league.ts`, `season.ts`, `draft-pick.ts`) + +Always query through the model layer — never write Drizzle queries directly in route files. + +## Frontend + +- **Routes config**: `app/routes.ts` — all routes must be explicitly registered here (React Router 7 does not auto-discover files) +- **Route files**: `app/routes/` — file naming uses `.` as path separator; `$param` for dynamic segments; `_index.tsx` for index routes +- **Components**: `app/components/` — UI primitives in `app/components/ui/` (ShadCN) +- **Hooks**: `app/hooks/` — custom React hooks +- **Lib**: `app/lib/` — pure utility functions + +## Real-time (Socket.IO) + +Clients join a room per season. Key events: + +| Event | Direction | Trigger | +|---|---|---| +| `join-draft` | client → server | on draft page load | +| `timer-update` | server → client | every second during active draft | +| `pick-made` | server → client | participant drafted | +| `autodraft-updated` | server → client | team enables/disables autodraft | +| `team-connected` / `team-disconnected` | server → client | presence | + +Server emits via `getSocketIO()` from `server/socket.ts`. Clients consume via `useDraftSocket()` hook at `app/hooks/useDraftSocket.ts`. + +### Adding a Socket Event + +1. Define types in `server/socket.d.ts` +2. Add handler in `server/socket.ts` +3. Emit from server: `getSocketIO().to(room).emit(...)` +4. Listen in client component or hook diff --git a/docs/agents/auth.md b/docs/agents/auth.md new file mode 100644 index 0000000..8ee2ab2 --- /dev/null +++ b/docs/agents/auth.md @@ -0,0 +1,7 @@ +# Authentication + +- Auth is handled by Clerk (`@clerk/react-router`) +- User records are synced from Clerk via webhook at `routes/api/webhooks/clerk.ts` +- Ownership validation: check `ownerId` field against the Clerk user ID (`clerkId`) +- Admin access: `isAdmin` boolean on the users table; all `/admin` routes enforce this +- League commissioners: stored separately in the `commissioners` table (not via `isAdmin`) diff --git a/docs/agents/database.md b/docs/agents/database.md new file mode 100644 index 0000000..6a6d64f --- /dev/null +++ b/docs/agents/database.md @@ -0,0 +1,35 @@ +# Database + +## Schema Changes Workflow + +1. Edit `database/schema.ts` +2. `npm run db:generate` — generates migration SQL + snapshot +3. Verify the SQL in `drizzle/` looks correct +4. Verify a snapshot was created in `drizzle/meta/` (e.g. `0070_snapshot.json`) +5. `npm run db:migrate` — applies migration +6. Add model functions in `app/models/` for new queries + +## Drizzle Migration Rules + +**NEVER** manually create migration SQL files or edit `drizzle/meta/_journal.json`. The journal and snapshots must stay in sync — drizzle-kit manages both. Missing snapshots cause `drizzle-kit generate` to fail with "data is malformed". + +- Always use `drizzle-kit generate` (or `drizzle-kit generate --custom` for interactive rename prompts) +- Snapshot JSON must only contain PostgreSQL-valid fields — do not add `"autoincrement"` (Postgres uses sequences, and invalid fields cause Zod validation failures) +- After generating, run `drizzle-kit generate` again to confirm "No schema changes" — validates the snapshot chain +- For destructive migrations (drop/rename), use `IF EXISTS` / `IF NOT EXISTS` guards + +## Scoring Event Dates (Non-Obvious Gotcha) + +Event dates are stored in **two different columns** depending on event type. Any date-range query must account for both: + +1. **`scoringEvents.eventDate`** (`date` column, `YYYY-MM-DD`) — used for non-bracket events (majors, final standings) and sometimes bracket events when the admin sets one date for an entire round. + +2. **`playoffMatchGames.scheduledAt`** (`timestamp`) — used for individual games within a bracket match. Bracket events often have `eventDate = null` on the scoring event itself. + +**Pattern for date-range queries:** Two-step approach: +1. `selectDistinct` event IDs via left joins through `playoffMatches → playoffMatchGames`, filtering: `eventDate IN [dates] OR (scheduledAt >= dayStart AND scheduledAt <= dayEnd)` +2. `findMany` on the matched IDs to load full event data with relations + +All timestamp bounds use UTC (`T00:00:00.000Z` / `T23:59:59.999Z`). + +Reference implementation: `getEventsForDates` and `getUpcomingEventsForDraftedParticipants` in `app/models/scoring-event.ts`. diff --git a/docs/agents/domain-models.md b/docs/agents/domain-models.md new file mode 100644 index 0000000..2017c16 --- /dev/null +++ b/docs/agents/domain-models.md @@ -0,0 +1,34 @@ +# Domain Models + +## Fantasy Draft System + +| Model | Description | +|---|---| +| **League** | Top-level container; has one or more commissioners | +| **Season** | One iteration of a league. Status: `pre_draft` → `draft` → `active` → `completed` | +| **Team** | Belongs to a season; owned by a user (`ownerId` = Clerk user ID) | +| **Draft Slot** | Defines draft order for teams in a season | +| **Draft Pick** | Record of each pick made | +| **Draft Queue** | A team's pre-ordered wish list of participants | +| **Draft Timer** | Active countdown for the team currently on the clock | +| **Autodraft Settings** | Per-team config for automatic picking | + +Season status drives UI visibility: draft order shown in `pre_draft`, draft controls active in `draft`, etc. + +### Snake Draft Logic + +- Draft slots define base order (1, 2, 3…) +- Odd rounds go 1→N; even rounds go N→1 +- `currentPickNumber` on the season tracks position +- `server/timer.ts` handles autopicks when timer expires + +## Sports Data System + +| Model | Description | +|---|---| +| **Sport** | Base sport definition (e.g. NFL, NBA); type is `team` or `individual` | +| **Sports Season** | A specific season of a sport (e.g. "2024 NFL Season") | +| **Participant** | An athlete or team that can be drafted | +| **Season Template** | Reusable configuration for creating league seasons | +| **Season Sport** | Junction: links a fantasy season to the sports it covers | +| **Participant Result** | Final standings/scores for participants at season end | diff --git a/docs/agents/routing.md b/docs/agents/routing.md new file mode 100644 index 0000000..5d5b22b --- /dev/null +++ b/docs/agents/routing.md @@ -0,0 +1,32 @@ +# Routing + +## Adding a New Route + +Both steps are required — skipping step 1 causes a silent 404. + +1. **Register in `app/routes.ts`** (do this first): + ```typescript + route("admin/sports-seasons/:id/new-page", "routes/admin.sports-seasons.$id.new-page.tsx") + ``` + Nested admin routes go inside the `route("admin", ...)` array. + +2. **Create the file** in `app/routes/` with the matching name. + +3. Export `loader` / `action` as needed. TypeScript types are auto-generated via `react-router typegen`. + +## File Naming Conventions + +- `.` separates path segments: `leagues.$leagueId.settings.tsx` → `/leagues/:leagueId/settings` +- `$param` = dynamic segment +- `_index.tsx` = index route for a path + +## Admin Routes + +All admin routes are nested under `/admin` and require `isAdmin: true` on the user: + +| Prefix | Area | +|---|---| +| `admin.sports.*` | Sports management | +| `admin.sports-seasons.*` | Sports seasons | +| `admin.templates.*` | Season templates | +| `admin.data-sync.tsx` | Data sync utilities | diff --git a/docs/agents/testing.md b/docs/agents/testing.md new file mode 100644 index 0000000..c976099 --- /dev/null +++ b/docs/agents/testing.md @@ -0,0 +1,30 @@ +# Testing + +See `TESTING.md` at the project root for the full guide. + +## Required Coverage + +Tests are required for all new features. Do not consider a feature complete until tests are written and passing. + +| What you're adding | Required test | +|---|---| +| Model function | Unit test in co-located `__tests__/` directory | +| Route loader/action | Unit or integration test for the logic | +| Utility function | Unit test | +| Component with non-trivial logic | Component test (React Testing Library) | +| Critical user flow | Cypress E2E test | + +## Test Locations + +- **Unit / component tests**: `__tests__/` directories co-located with source files +- **E2E tests**: `cypress/e2e/` +- **Fixtures**: `app/test/fixtures/` + +## Commands + +```bash +npm run test:run # unit tests, run once +npm test # unit tests, watch mode +npm run test:e2e:headless # Cypress headless +npm run test:all # everything +```