Testing
瑞成 PMS 內建兩層測試套件:
| Tier | Tool | Coverage Gate | Scope |
|---|---|---|---|
| Unit | Vitest | ≥ 80% (db-free layer) | lib/**/*-utils.ts、lib/validations/、lib/rc-permissions.ts、純邏輯 |
| E2E | Playwright | N/A (smoke gate) | 工牌登入流程、儀表板、案件 CRUD、業主 CRM、權限守衛 |
Running Tests
All commands run from next-app/:
# Unit tests
pnpm test # run all unit tests
pnpm test:coverage # with v8 coverage report
# E2E tests
pnpm db:seed # required — seeds 管理員/行政/一般 三層 demo 帳號 + 瑞成 personas
pnpm test:e2e # Playwright (auto-boots dev server)Unit Tests (Vitest)
Unit tests live next to the code as lib/**/*.test.ts (e.g. lib/is-admin.test.ts, lib/api-keys-utils.test.ts, lib/validations/auth.test.ts).
The db-free *-utils.ts pattern
lib/db.ts throws if DATABASE_URL is unset, so any module that transitively imports db cannot be unit-tested in isolation. To keep logic testable without a database, pure logic is extracted into *-utils.ts files (no db import) and only those are imported by *.test.ts.
// lib/api-keys-utils.test.ts — pure functions, no DB needed
import { describe, expect, it } from 'vitest'
import { generateApiKey, parseApiKey, hashKey } from './api-keys-utils'
describe('api-keys-utils', () => {
it('hashKey is deterministic', () => {
expect(hashKey('abc')).toBe(hashKey('abc'))
})
})Rule of thumb: if a function needs the database, it belongs in the action/query layer (covered by e2e). If it's pure logic, put it in
*-utils.tsand unit-test it directly.
E2E Tests (Playwright)
E2E tests live in next-app/e2e/*.spec.ts and need a seeded database + a running dev server (the Playwright config auto-boots it locally).
# Run all e2e tests (chromium project)
pnpm test:e2e
# Run a specific spec
pnpm test:e2e --grep "dashboard"E2E specs live in next-app/e2e/*.spec.ts(工牌登入、儀表板、案件 CRUD、權限守衛等)。 There is also a visual-regression project: pnpm test:vrt (pnpm test:vrt:update to refresh snapshots).
測試帳號(工牌編號登入,pnpm db:seed 後可用):
| 角色 | 工牌編號 | 密碼 |
|---|---|---|
| 管理員 Admin | R90001 | Admin123! |
| 行政 Operator | R90002 | Editor123! |
| 一般 Staff | R90003 | Viewer123! |
Coverage Gate
The 80% coverage gate covers the db-free layer and is enforced by the QA agent (/athena:qa --test-only). It runs pnpm test:coverage and blocks merge if coverage drops below threshold.
# Check coverage manually
pnpm test:coverage
# Look for: Statements / Functions ≥ 80%