style(13-03): apply Prettier formatting across repo

Mechanical reformat — no logic changes. 398 files changed, 19125
insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc
(singleQuote:true, semi:true, tabWidth:2, trailingComma:all,
printWidth:100). Isolated per D-13-08 for reviewability.
This commit is contained in:
Lucas Berger
2026-06-11 20:35:18 -04:00
parent 4bc0445173
commit 982438dc10
398 changed files with 19050 additions and 16382 deletions
+113 -67
View File
@@ -5,16 +5,19 @@
## Test Framework
**Runner:**
- Backend: Vitest 4.1.8, Node environment
- Frontend: Vitest 4.1.8, jsdom environment
- Config: `apps/api/vitest.config.ts`, `apps/pwa/vitest.config.ts`
**Assertion Library:**
- Vitest built-in `expect()`
- Testing Library (`@testing-library/react`, `@testing-library/jest-dom`) for component DOM assertions
- `jest-dom` matchers extended via `apps/pwa/src/test-setup.ts`
**Run Commands:**
```bash
# Run all tests
pnpm test
@@ -30,14 +33,17 @@ vitest run --coverage
## Test File Organization
**Location:**
- Backend: `apps/api/tests/` parallel to `apps/api/src/` — mirrors source structure
- Frontend: Co-located with source files — `src/components/Foo.tsx``src/components/Foo.test.tsx`
**Naming:**
- Test files: `{module}.test.ts` or `.test.tsx`
- Fixtures: `apps/api/tests/fixtures/` — fixture files (e.g., `weekly-dst.ics`) loaded by test helpers
**Structure:**
```
apps/api/tests/
├── health.test.ts # End-to-end test for GET /health
@@ -76,39 +82,41 @@ apps/pwa/src/
## Test Structure
**Suite Organization:**
```typescript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
describe('GET /health', () => {
it('returns 200 with { ok: true, db: "up" } when DB round-trip succeeds', async () => {
// Arrange
const { app } = await import('../src/index.js')
const { app } = await import('../src/index.js');
// Act
const res = await app.request('/health')
const res = await app.request('/health');
// Assert
expect(res.status).toBe(200)
const body = await res.json() as { ok: boolean; db: string }
expect(body.ok).toBe(true)
})
expect(res.status).toBe(200);
const body = (await res.json()) as { ok: boolean; db: string };
expect(body.ok).toBe(true);
});
it('returns 503 when DB round-trip throws', async () => {
// Arrange
const { db } = await import('../src/db/client.js')
vi.mocked(db.execute).mockRejectedValueOnce(new Error('DB connection failed'))
const { db } = await import('../src/db/client.js');
vi.mocked(db.execute).mockRejectedValueOnce(new Error('DB connection failed'));
// Act
const { app } = await import('../src/index.js')
const res = await app.request('/health')
const { app } = await import('../src/index.js');
const res = await app.request('/health');
// Assert
expect(res.status).toBe(503)
})
})
expect(res.status).toBe(503);
});
});
```
**Patterns:**
- Async test functions with full await chain
- Hono request testing: `app.request(path)` returns a Response object
- Mock setup in `beforeEach`; cleanup in `afterEach` with `vi.unstubAllGlobals()` or `vi.clearAllMocks()`
@@ -120,25 +128,27 @@ describe('GET /health', () => {
**Framework:** Vitest `vi` object (`vi.mock`, `vi.mocked`, `vi.fn`, `vi.stubGlobal`)
**Module Mocking:**
```typescript
// Hoist vi.mock() calls to the top of the module (Vitest requirement)
vi.mock('../src/db/client.js', () => ({
db: {
execute: vi.fn().mockResolvedValue([[{ '1': 1 }]]),
},
}))
}));
```
**Function Mocking:**
```typescript
const mockFetch = vi.mocked(fetch)
const mockFetch = vi.mocked(fetch);
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ uid: 'test-uid' }),
} as Response)
} as Response);
// Call the function under test
await createEvent(payload)
await createEvent(payload);
// Assert the mock was called correctly
expect(mockFetch).toHaveBeenCalledWith(
@@ -147,27 +157,30 @@ expect(mockFetch).toHaveBeenCalledWith(
method: 'POST',
credentials: 'include',
}),
)
);
```
**Global Stubs (Frontend):**
```typescript
beforeEach(() => {
vi.stubGlobal('fetch', vi.fn())
})
vi.stubGlobal('fetch', vi.fn());
});
afterEach(() => {
vi.unstubAllGlobals()
})
vi.unstubAllGlobals();
});
```
**What to Mock:**
- External I/O: database (via `vi.mock` on `src/db/client.js`)
- Network calls: `fetch` (via `vi.stubGlobal('fetch', ...)`)
- Environment-dependent code: `window.matchMedia` (jsdom polyfill, see test-setup.ts)
- Time-dependent code: `Date`, `setTimeout` (if needed; not used currently)
**What NOT to Mock:**
- Pure utility functions — test them directly (colorUtils, eventDateTime, hydrateEvents)
- Zod validation schemas — test with real payloads
- Zustand stores — instantiate real store, call real methods
@@ -181,7 +194,7 @@ Fixture files are `.ics` (iCalendar) strings stored in `apps/api/tests/fixtures/
```typescript
// Load fixture file
const rawVevent = readFileSync(join(FIXTURES, 'weekly-dst.ics'), 'utf8')
const rawVevent = readFileSync(join(FIXTURES, 'weekly-dst.ics'), 'utf8');
// Use in test
const occurrences = expandOccurrences(
@@ -194,7 +207,7 @@ const occurrences = expandOccurrences(
'Alice',
'#4A90D9',
false,
)
);
```
**Test Data (Frontend):**
@@ -205,14 +218,25 @@ vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => ({
calendars: [
{ url: 'https://caldav.fastmail.com/cal1', displayName: 'My Calendar', color: '#4A90D9', isShared: false },
{ url: 'https://caldav.fastmail.com/cal2', displayName: 'Family', color: '#F25C7A', isShared: true },
{
url: 'https://caldav.fastmail.com/cal1',
displayName: 'My Calendar',
color: '#4A90D9',
isShared: false,
},
{
url: 'https://caldav.fastmail.com/cal2',
displayName: 'Family',
color: '#F25C7A',
isShared: true,
},
],
}),
} as Response)
} as Response);
```
**Location:**
- Fixture files: `apps/api/tests/fixtures/` — raw .ics strings for iCalendar tests
- Mock payloads: inline in test files (`api/client.test.ts`, etc.)
@@ -221,10 +245,12 @@ vi.mocked(fetch).mockResolvedValueOnce({
**Requirements:** None enforced (no coverage thresholds in vitest.config.ts)
**Current State:**
- Backend: Partial coverage — broker modules (expand, sync, write, crypto, vevent) tested; route handlers mostly untested
- Frontend: Good coverage of utility functions (colorUtils, eventDateTime, hydrateEvents, calendarConfig) and API client
**View Coverage:**
```bash
# Generate coverage report (requires @vitest/coverage-v8)
vitest run --coverage
@@ -233,38 +259,43 @@ vitest run --coverage
## Test Types
**Unit Tests:**
- Scope: Single function or small module in isolation (mocks external dependencies)
- Approach: Test input → output contracts, edge cases, error conditions
- Examples: `lib/colorUtils.test.ts`, `broker/crypto.test.ts`, `api/client.test.ts`
**Integration Tests:**
- Scope: Multi-module behavior (e.g., route handler + DB + auth middleware)
- Approach: Test realistic user flows using `app.request()` for HTTP semantics
- Examples: `health.test.ts` (GET /health with mocked DB)
- No external API calls (Fastmail, Authelia mocked)
**E2E Tests:**
- Not implemented; would require running a real server + browser
- Currently using `playwright-cli` skill for browser-based smoke tests of UI (per project CLAUDE.md)
## Common Patterns
**Async Testing:**
```typescript
it('returns { uid } on success', async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => ({ uid: 'returned-uid' }),
} as Response)
} as Response);
const { createEvent } = await import('./client.js')
const result = await createEvent(payload)
const { createEvent } = await import('./client.js');
const result = await createEvent(payload);
expect(result).toEqual({ uid: 'returned-uid' })
})
expect(result).toEqual({ uid: 'returned-uid' });
});
```
**Error Testing:**
```typescript
it('throws on non-ok response', async () => {
vi.mocked(fetch).mockResolvedValueOnce({
@@ -274,7 +305,7 @@ it('throws on non-ok response', async () => {
} as Response)
const { createEvent } = await import('./client.js')
await expect(
createEvent({ title: '', ... })
).rejects.toThrow()
@@ -282,78 +313,89 @@ it('throws on non-ok response', async () => {
```
**Status Code Testing:**
```typescript
it('returns 503 when DB round-trip throws', async () => {
const { db } = await import('../src/db/client.js')
vi.mocked(db.execute).mockRejectedValueOnce(new Error('DB connection failed'))
const { db } = await import('../src/db/client.js');
vi.mocked(db.execute).mockRejectedValueOnce(new Error('DB connection failed'));
const { app } = await import('../src/index.js')
const res = await app.request('/health')
const { app } = await import('../src/index.js');
const res = await app.request('/health');
expect(res.status).toBe(503)
})
expect(res.status).toBe(503);
});
```
**Fixture-Based Testing:**
```typescript
describe('expandOccurrences — DST correctness', () => {
it('returns 10:00 America/New_York wall-clock time on BOTH sides of March 2026 DST boundary', () => {
const rawVevent = loadFixture('weekly-dst.ics')
const windowStart = new Date('2026-03-01T00:00:00Z')
const windowEnd = new Date('2026-04-01T00:00:00Z')
const rawVevent = loadFixture('weekly-dst.ics');
const windowStart = new Date('2026-03-01T00:00:00Z');
const windowEnd = new Date('2026-04-01T00:00:00Z');
const occurrences = expandOccurrences(
rawVevent,
windowStart,
windowEnd,
1, 'My Calendar', 1, 'Alice', '#4A90D9', false,
)
1,
'My Calendar',
1,
'Alice',
'#4A90D9',
false,
);
// Check DST correctness: all occurrences must show hour === 10 local time
for (const occ of occurrences) {
expect(occ.start).toMatch(/T10:00:00/)
expect(occ.start).toContain('[America/New_York]')
expect(occ.start).toMatch(/T10:00:00/);
expect(occ.start).toContain('[America/New_York]');
}
// Explicitly check pre- and post-transition occurrences
const preTransition = occurrences.find(o => o.start.includes('2026-03-01'))
const postTransition = occurrences.find(o => o.start.includes('2026-03-15'))
const preTransition = occurrences.find((o) => o.start.includes('2026-03-01'));
const postTransition = occurrences.find((o) => o.start.includes('2026-03-15'));
expect(preTransition!.start).toContain('-05:00[America/New_York]') // EST
expect(postTransition!.start).toContain('-04:00[America/New_York]') // EDT
})
})
expect(preTransition!.start).toContain('-05:00[America/New_York]'); // EST
expect(postTransition!.start).toContain('-04:00[America/New_York]'); // EDT
});
});
```
**Zustand Store Testing:**
```typescript
describe('calendarStore', () => {
it('setEventForm(true, edit, some-uid) updates all three keys', async () => {
const { useCalendarStore } = await import('../store/calendarStore.js')
useCalendarStore.getState().setEventForm(true, 'edit', 'some-uid')
const state = useCalendarStore.getState()
const { useCalendarStore } = await import('../store/calendarStore.js');
useCalendarStore.getState().setEventForm(true, 'edit', 'some-uid');
const state = useCalendarStore.getState();
expect(state.eventFormOpen).toBe(true)
expect(state.eventFormMode).toBe('edit')
expect(state.eventFormUid).toBe('some-uid')
})
})
expect(state.eventFormOpen).toBe(true);
expect(state.eventFormMode).toBe('edit');
expect(state.eventFormUid).toBe('some-uid');
});
});
```
## Test Setup
**Backend (Node environment):**
- `vitest.config.ts` specifies `environment: 'node'` with `globals: true`
- No test-setup file needed (Node has built-in globals)
- Modules imported via `await import(...)` to enable per-test mocking
**Frontend (jsdom environment):**
- `vitest.config.ts` specifies `environment: 'jsdom'` with `globals: true` and `setupFiles: ['./src/test-setup.ts']`
- `test-setup.ts` polyfills `window.matchMedia` (jsdom doesn't implement CSSOM MediaQueryList)
- `test-setup.ts` extends `expect` with `jest-dom` matchers
- Timezone pinned to UTC via `env: { TZ: 'UTC' }` for deterministic date tests (WR-05)
**Example (from `apps/pwa/vitest.config.ts`):**
```typescript
export default defineConfig({
test: {
@@ -362,12 +404,13 @@ export default defineConfig({
setupFiles: ['./src/test-setup.ts'],
env: { TZ: 'UTC' },
},
})
});
```
**Example (from `apps/pwa/src/test-setup.ts`):**
```typescript
import '@testing-library/jest-dom'
import '@testing-library/jest-dom';
Object.defineProperty(window, 'matchMedia', {
writable: true,
@@ -376,25 +419,28 @@ Object.defineProperty(window, 'matchMedia', {
media: query,
// ... other MediaQueryList methods
}),
})
});
```
## Known Testing Gaps
**Backend Route Handlers:**
- GET /api/events, POST /api/events/create, PATCH /api/events/:uid/edit, DELETE /api/events/:uid — no route tests yet (in scope for Phase 5 / Plan 05)
- GET /api/events/writable-calendars, GET /api/events/sync-status — no route tests
- SSE route (`/api/sse`) — not tested
- Auth flow tests (dev-bypass, OIDC session) partially covered; integration tests with Authelia not applicable
**Frontend Components:**
- EventForm, DeleteConfirmationDialog, CalendarShell — no component tests yet
- SSE event listener integration (real-time list updates) — not tested
**Integration:**
- Full end-to-end flow (login → fetch events → create event → poll sync-status) — not covered
- Database transaction rollback on error — not explicitly tested
---
*Testing analysis: 2026-06-09*
_Testing analysis: 2026-06-09_