Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .claude/rules/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,52 @@ E2E files: **must** use `.spec.ts` extension (`.test.ts` not detected).

Route unit tests: `src/routes/**/*.test.ts` is included by `vite.config.ts`. **Never use `+` as a filename prefix** — SvelteKit reserves it and `pnpm check` will error. Name route test files `page_server.test.ts`, not `+page.server.test.ts`.

## Test Environment

Default is `node` (set in `vite.config.ts`). Only files touching the real DOM (`window` / `document` / `localStorage`) opt in with a top-of-file `// @vitest-environment jsdom`. **Never set jsdom globally** — most tests are pure units and per-file jsdom construction is ~5.5x slower.

### Toggling `browser` per describe

**Never register `vi.mock('$app/environment')` twice in one file.** Every `vi.mock` is hoisted above the imports and runs once before any test, so a `{ browser: false }` mock written inside an SSR `describe`/`beforeEach` silently overwrites the `{ browser: true }` one at the top — the whole file ends up pinned to `browser = false`, the localStorage branches never execute, and tests that "verify" them become false-positives while still passing.

Use **one dynamic mock** driven by a `vi.hoisted` flag, and toggle the flag in `beforeEach`. Default the flag to `false` so singletons constructed at import time stay SSR-safe:

```typescript
// @vitest-environment jsdom
const browserState = vi.hoisted(() => ({ value: false }));

vi.mock('$app/environment', () => ({
get browser() {
return browserState.value;
},
}));

describe('MyStore', () => {
let store: MyStore;

beforeEach(() => {
browserState.value = true;
localStorage.clear();
store = new MyStore(); // constructed after the flag flips — reads localStorage
});
// …
});

describe('MyStore in SSR', () => {
beforeEach(() => {
browserState.value = false;
});
// …
});
```

**Never assert a browser branch on the import-time singleton.** The flag is `false` while the module graph is evaluated, so the exported singleton is always built in SSR mode and never touches localStorage — asserting on it under `browser = true` is the same false-positive as the double-`vi.mock` above. Construct a fresh instance inside the browser `describe` (which is why the store class, not just the singleton, needs a named export). The singleton is still worth one assertion: that import-time construction is SSR-safe.

In jsdom files, do **not** stub localStorage with `vi.stubGlobal` — use jsdom's real `Storage` and assert on state (`localStorage.getItem(key)`), not on spy calls. Cover both SSR guards with separate cases, since one assertion cannot carry both:

- **read guard**: pre-seed localStorage, construct a fresh store, expect the _default_ (the pre-seeded value stays in storage, so do not expect `getItem(key)` to be null here)
- **write guard**: start from empty localStorage, call the setter, expect `getItem(key)` to still be null

## Unit Testing Patterns

### Assertions
Expand Down
4 changes: 2 additions & 2 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ services:
- '5555:5555'
tty: true
volumes:
- .:/usr/src/app:cached
- ./node_modules:/usr/src/app/node_modules:cached
- .:/usr/src/app
- ./node_modules:/usr/src/app/node_modules
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://db_user:db_password@db:5432/test_db?pgbouncer=true&connection_limit=10&connect_timeout=60&statement_timeout=60000 # Note: Local server cannot start if port is set to db:6543.
Expand Down
3 changes: 0 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
"@sveltejs/vite-plugin-svelte": "7.2.0",
"@tailwindcss/forms": "0.5.11",
"@tailwindcss/vite": "4.3.2",
"@testing-library/jest-dom": "6.9.1",
"@types/gtag.js": "0.0.20",
"@types/jsdom": "28.0.3",
"@typescript-eslint/eslint-plugin": "8.63.0",
Expand Down Expand Up @@ -77,8 +76,6 @@
"@lucia-auth/adapter-prisma": "3.0.2",
"@lucide/svelte": "1.24.0",
"@prisma/client": "5.22.0",
"@testing-library/svelte": "5.4.2",
"@types/jest": "30.0.0",
"@types/node": "25.9.5",
"debug": "4.4.3",
"lucia": "2.7.7",
Expand Down
Loading
Loading