11 KiB
Testing
Test framework and setup
Both apps use Vitest (^4.1.8).
| App | Environment | Setup file |
|---|---|---|
apps/api |
node |
apps/api/test/setup.ts |
apps/pwa |
jsdom |
apps/pwa/src/test-setup.ts |
apps/api setup (test/setup.ts) registers a global afterEach that truncates list_items, list_shares, push_subscriptions, and lists in FK-safe order after every test. This keeps DB-backed integration tests isolated without requiring a full DB reset between runs. Parallel file execution is disabled (fileParallelism: false) to prevent FK violations when multiple test files share the same MariaDB.
apps/pwa setup (src/test-setup.ts) imports @testing-library/jest-dom for extended matchers and polyfills window.matchMedia for jsdom (required because Zustand's calendarStore calls window.matchMedia at module initialisation time). The timezone is pinned to UTC via env: { TZ: 'UTC' } so date-extraction assertions are deterministic across developer machines and CI.
No additional install step is needed beyond the normal pnpm install at the repo root.
Running tests
Unit and integration tests
All API tests (from repo root):
pnpm --filter @familysync/api test
This is also the command run by pnpm test at the root.
All PWA unit tests:
pnpm --filter @familysync/pwa test
Watch mode (API):
pnpm --filter @familysync/api test:watch
Single test file:
pnpm --filter @familysync/api exec vitest run tests/routes/lists.test.ts
End-to-end tests (Playwright)
The PWA has a Playwright harness configured in apps/pwa/playwright.config.ts with two device profiles:
| Profile | Viewport | Engine | User-Agent |
|---|---|---|---|
iphone |
390×844 | WebKit | Mobile Safari (iPhone 14) |
pixel |
412×915 | Chromium | Chrome Android (Pixel 7) |
Both profiles block the service worker (serviceWorkers: 'block') so the Workbox SW does not intercept requests during tests. Auth is handled via DEV_AUTH_BYPASS=true on the API — never via stored browser state.
Run all e2e tests (both profiles):
pnpm test:e2e
# or
pnpm --filter @familysync/pwa test:e2e
Run a single profile:
pnpm --filter @familysync/pwa exec playwright test --project=pixel
pnpm --filter @familysync/pwa exec playwright test --project=iphone
Interactive UI mode:
pnpm --filter @familysync/pwa test:e2e:ui
The baseURL is driven by PLAYWRIGHT_BASE_URL (default: http://localhost:5173). In local mode the config reuses a running Vite dev server; in CI it starts Vite itself. The API, MariaDB, and Redis must already be running via Docker Compose before launching e2e tests locally — see docs/DEVELOPMENT.md.
Type checking (separate from tests — required)
Vitest uses esbuild, which strips TypeScript types at runtime. A test run can pass while tsc reports errors. Always run type checks separately:
pnpm typecheck # runs tsc --noEmit across both apps
pnpm --filter @familysync/api typecheck
pnpm --filter @familysync/pwa typecheck
The PWA typecheck also covers the e2e spec files: tsc --project tsconfig.e2e.json --noEmit.
Quality gate
The full local quality gate before opening a PR:
pnpm lint && pnpm format:check && pnpm typecheck && pnpm test
Add e2e when changing PWA behaviour:
pnpm test:e2e
| Step | Command | What it checks |
|---|---|---|
| Lint | pnpm lint |
ESLint --max-warnings 0 across both apps (type-aware) |
| Format check | pnpm format:check |
Prettier — fails on any unformatted file |
| Typecheck | pnpm typecheck |
tsc --noEmit across both apps (including e2e tsconfig) |
| Unit / API tests | pnpm test |
API integration tests via Vitest |
| PWA unit tests | pnpm --filter @familysync/pwa test |
Component and logic tests in jsdom |
| E2E | pnpm test:e2e |
Playwright iphone + pixel profiles |
A deliberate ESLint violation makes pnpm lint exit non-zero; a formatting deviation makes pnpm format:check exit non-zero. Both block the PR in CI.
To auto-fix formatting locally:
pnpm format # prettier --write .
Integration tests requiring a real database
Several API tests in apps/api/tests/lib/ and apps/api/tests/routes/ connect to the real dev MariaDB rather than mocking the DB layer. These tests require the dev Docker stack to be running with port 3306 exposed.
Start the dev stack:
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d mariadb
Set environment variables, then run:
set -a; . ./.env; set +a
export DB_HOST=127.0.0.1 DB_PORT=3306
pnpm --filter @familysync/api test
DB-backed tests that require this setup include:
apps/api/tests/lib/listAccess.test.ts—getAccessibleListIdsaccess-scope queriesapps/api/tests/lib/listChangeDispatcher.test.ts— list change dispatcher with real DB rowsapps/api/tests/routes/lists.test.ts— full lists API router (creates/deletes real rows)
Pure-logic tests (e.g. apps/api/tests/broker/expand.test.ts, apps/api/tests/lib/rank.test.ts) do not require DB — the afterEach cleanup is a no-op when tables are empty or no DB connection is available.
Writing new tests
File naming and location
| App | Convention | Example |
|---|---|---|
apps/api |
apps/api/tests/{category}/*.test.ts |
apps/api/tests/routes/push.test.ts |
apps/pwa |
co-located *.test.ts / *.test.tsx |
src/components/AppNav.test.tsx |
apps/pwa |
e2e specs | e2e/*.spec.ts |
Test categories for apps/api:
apps/api/tests/auth/— authentication middleware and session handlingapps/api/tests/broker/— CalDAV sync, outbox worker, event expansion, push dispatchapps/api/tests/lib/— pure library functions and service logicapps/api/tests/routes/— HTTP route integration testsapps/api/tests/health.test.ts— health check endpointapps/api/tests/fixtures/— shared.icsfixture files and DB fixture helpers
Test helpers
apps/api/tests/helpers/db.ts—createMockDb()returns a Vitest mock of the Drizzledbsingleton; also exports sample VEVENT strings (SAMPLE_VEVENT_TIMED,SAMPLE_VEVENT_ALLDAY,SAMPLE_VEVENT_RECURRING_TIMED,SAMPLE_VEVENT_RECURRING_ALLDAY) for broker tests.apps/api/tests/fixtures/*.ics— Raw iCalendar fixture files for broker parsing tests (allday-birthday.ics,exdate-series.ics,single-duration.ics,weekly-count3.ics,weekly-dst.ics).apps/api/tests/fixtures/vapid.ts— VAPID key fixture for push tests.apps/pwa/src/test-setup.ts— ProvidesmatchMediapolyfill and jest-dom matchers for all PWA tests automatically viasetupFiles.
For PWA component tests, use @testing-library/react (^16.3.0) render helpers. Import from vitest for assertions — @testing-library/jest-dom matchers are available globally via the setup file.
Coverage requirements
No coverage thresholds are configured in either vitest.config.ts. There is no minimum coverage enforcement in CI.
CI integration
CI runs on a self-hosted Gitea Actions runner and triggers on every pull request targeting main (.gitea/workflows/ci.yml). Three jobs run in parallel:
fast-checks
Runs lint, format check, typecheck, and PWA unit tests — no external services required.
| Step | Command |
|---|---|
| Lint | pnpm lint |
| Format check | pnpm format:check |
| Typecheck | pnpm typecheck |
| PWA unit tests | pnpm --filter @familysync/pwa test |
api
Runs the full API test suite against a mariadb:11 service container.
| Step | Detail |
|---|---|
| MariaDB service | mariadb:11 container; DB_HOST=mariadb, DB_PORT=3306 |
| Readiness poll | Node script via mysql2 driver (no mysql CLI in runner) |
| Schema migrations | pnpm --filter @familysync/api db:migrate |
| Tests | pnpm --filter @familysync/api test |
The throwaway credentials (DB_USER=familysync, DB_PASSWORD=testpass) are scoped to the ephemeral CI container and are never production secrets.
actions/cache@v4 is intentionally omitted — the cache server times out on this runner (socket hang-up). pnpm install without cache takes ~30 s and is acceptable.
harness
Runs the Playwright mobile e2e harness (iphone + pixel) against a runner-hosted dev stack.
| Step | Detail |
|---|---|
| MariaDB service | Same mariadb:11 setup as the api job |
| Schema migrations | pnpm --filter @familysync/api db:migrate |
| Dev user seed | Inserts users row id=1 (INSERT IGNORE) for DEV_AUTH_BYPASS |
| API build | pnpm --filter @familysync/api build (dist/ is gitignored) |
| Playwright install | npx playwright install --with-deps webkit chromium (no cache) |
| API start + tests | API started as a background process in the same step as playwright test to survive the step boundary; DEV_AUTH_BYPASS=true, NODE_ENV=development |
| Base URL | http://127.0.0.1:5173 (not localhost — runner resolves localhost to ::1 but Vite binds IPv4-only) |
| Artifacts on fail | Traces, screenshots, videos, and HTML report uploaded via ChristopherHX/gitea-upload-artifact@v4 (standard upload-artifact aborts on Gitea) |
The API process is started and the Playwright suite invoked within a single CI step. Starting the API in an earlier step causes it to be reaped at the step boundary before Playwright runs.