Testing
瑞成 PMS 內建三層測試套件:
| Tier | Tool | Coverage Gate | Scope |
|---|---|---|---|
| Unit | Vitest (vitest.config.ts) | ≥ 80% (db-free layer) | lib/**/*-utils.ts、lib/validations/、lib/rc-permissions.ts、純邏輯、守門測試 |
| Integration | Vitest (vitest.int.config.ts) | N/A | Server Actions / schema / migration 對真的 Postgres(pnpm test:int) |
| E2E | Playwright | N/A (smoke gate) | 工牌登入流程、儀表板、案件 CRUD、業主 CRM、權限守衛 |
合併前的一鍵關卡(v1.3.0 起)
整套本機關卡一個指令跑完——typecheck · lint · unit · integration · pre-merge-check · 完整 chromium e2e, 且自己起一個用完即丟的 Postgres(由 Docker 指派連接埠,不碰你本機的 :5432):
scripts/verify/run-gates.sh --pg-port auto --app-port auto # 從 repo 根目錄;也可指定 --pg-port 554xx --app-port 34xx- e2e 前會等機器安靜、選好 app 連接埠、清掉
.next/dev;失敗的 e2e 只重跑一次,第二次過關報 FLAKY(保留第一次的 log);仍失敗則列出失敗的測試名稱。 - 要把子代理(subagent)的 PR 驗到「合併後的樹也綠」,用
scripts/verify/intake-pr.sh <PR#> <E###> [--merge]:先把 main 併進來(EPIC_INDEX/epic-progress/CHANGELOG/bugfix-log 衝突自動處理)、跑上面整套關卡、gate 後若 main 又前進就補跑,再用gh pr merge --squash --match-head-commit <sha>合併(避免合到過期的 head)。合併一律需要使用者明確授權。 - GitHub Actions 目前因帳務問題無法執行(2026-10-06 起),所以「本機完整關卡」就是唯一的驗證;修好之後記得在 main 補跑一次 CI。
守門測試(guard tests)
這幾支測試不驗功能,而是防止已修過的缺陷復發,新增日期/匯出/測試設定時請留意:
| 測試 | 防止什麼 |
|---|---|
app/frozen-today-guard.test.ts | 在模組層(行首)呼叫 getRcToday()/new Date()/Date.now() → 「今天」凍結在部署日(E385)。必須在函式/請求內求值。 |
test/rtl-async-timeout.test.tsx | 測試設定 asyncUtilTimeout 必須是 5000 ms(預設 1000 ms 在機器忙碌時會讓 waitFor 偶發失敗)。設定在 test/setup.ts。 |
lib/download-csv.test.ts | 所有 CSV 下載必須帶 UTF-8 BOM(E398)。 |
scripts/checks/check-schema-doc.sh(pre-merge Gate 8) | lib/schema/*.ts 的每張表都要出現在 docs/architecture/db-schema-relationships.md。 |
Running Tests
All commands run from next-app/:
# Unit tests
pnpm test # run all unit tests
pnpm test:coverage # with v8 coverage report
# Integration tests(需要一個 throwaway Postgres;用 run-gates.sh 最省事)
pnpm test:int
# E2E tests — 使用**自己的資料庫** saas_dev_e2e(E337),永遠不會碰 saas_dev
pnpm db:e2e-setup # drop → create → migrate → seed(先設 E2E_BASE_DATABASE_URL 指向本專案的 Postgres)
pnpm test:e2e # Playwright (auto-boots dev server;啟動前會驗證 :3000 是「這個 checkout」)機器上常有別的專案的 Postgres 佔著
:5432(例如上游 template 的容器)。不要假設:5432是本專案的: 一律用docker ps確認,或直接用run-gates.sh的 throwaway 資料庫。Docker 指派連接埠時不要手選 ≥49152 的埠(macOS 暫時埠範圍會綁定失敗)。
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%