climb_stairs

測試計畫 (Test Plan)

Test Plan — Ladder Room Online

Version: v1.0

Date: 2026-04-21

Based on: PRD v1.0, EDD v2.0, API v2.1

Author: AI QA Agent(devsop-autodev STEP-14)


§1 Test Strategy Overview

§1.1 測試目標

確保 Ladder Room Online 在上線前達到以下品質閘門:

§1.2 測試層次金字塔

           ┌─────────────────────────────┐
           │        E2E (Playwright)      │ 10%
           │  完整遊戲流程 × 多瀏覽器       │
           ├─────────────────────────────┤
           │   Performance (k6/Autocannon)│ 5%
           │   WS 並發壓測 × Lighthouse   │
           ├─────────────────────────────┤
           │  Integration (Vitest+TC)     │ 20%
           │  REST API × WS × Redis 真機  │
           ├─────────────────────────────┤
           │      Unit (Vitest)           │ 65%
           │  純函式 × 狀態機 × UI 邏輯   │
           └─────────────────────────────┘
層次工具環境目標覆蓋率
UnitVitest 1.xLocal(node)shared ≥ 90%;server ≥ 80%;client ≥ 70%
IntegrationVitest + testcontainersLocal(Docker Redis)所有 REST + WS 路由
E2EPlaywright 1.xLocal(K8s / Docker Compose)所有 P0 User Story Happy Path
Performancek6 / Autocannon / Lighthouse CILocal(k8s)+ CIPRD §2.1 KPI 全項
UAT手動 + 自動化腳本Staging / Local所有 PRD User Story AC

§2 Unit Test Plan

§2.1 packages/shared

測試檔案位置: packages/shared/src/tests/

§2.1.1 PRNG 模組(mulberry32, djb2, fisherYates)

測試案例說明AC/FR
djb2 — 已知輸入輸出驗證固定字串 → 固定 uint32(快照測試)FR-04-1
djb2 — 空字串回傳 5381hash 初始值邊界FR-04-1
djb2 — 長字串輸入不 overflow>>> 0 確保 uint32 範圍FR-04-1
Mulberry32 — 相同 seed 序列可重現連續呼叫 100 次產生相同序列FR-04-1
Mulberry32 — 輸出範圍 [0, 1)所有輸出值嚴格在此區間FR-04-1
Mulberry32 — 不同 seed 產生不同序列seed=0 vs seed=1 第一個值不同FR-04-1
fisherYates — 無元素遺失輸入 N 元素,輸出仍含全部 N 元素FR-04-4
fisherYates — 單元素陣列不變edge: N=1FR-04-4
fisherYates — 空陣列不報錯edge: N=0FR-04-4
fisherYates — 同 rng 產生相同排列determinism 驗證FR-04-4

§2.1.2 GenerateLadder — seed determinism

測試案例說明AC/FR
相同 seedSource + N → 完全相同輸出snapshot testFR-04-1, AC-H03-3
N=2, seed=0 → rowCount=20clamp 下界AC-H03-5, FR-04-3
N=7, seed=X → rowCount=21N×3=21,clamp(21,20,60)=21AC-H03-5, FR-04-3
N=21, seed=X → rowCount=60clamp 上界AC-H03-5, FR-04-3
N=100, seed=X → rowCount=60超過上界仍 clamp 至 60FR-04-3

§2.1.3 GenerateLadder — rowCount clamping

測試案例說明AC/FR
N=3 → rowCount=20(min clamp)N×3=9 < 20,取 20AC-H03-5
N=10 → rowCount=30N×3=30,無 clampAC-H03-5
N=20 → rowCount=60(max clamp)N×3=60,等於上界AC-H03-5

§2.1.4 GenerateLadder — segment validity

測試案例說明AC/FR
所有 segment.col 在 [0, N-2]橫槓左端不超出欄位FR-04-2
所有 segment.row 在 [0, rowCount-1]列索引合法FR-04-2
同 row 內 segment 無重疊(col 及 col+1 不交叉)防衝突規則FR-04-2
N=2 時 segment 數量 ≤ rowCount極端情況:每 row 最多 1 橫槓FR-04-2
N=50 時所有橫槓合法大 N stressFR-04-2

§2.1.5 GenerateLadder — bar density

測試案例說明AC/FR
N=2(possiblePositions=1)density=0.50,平均橫槓數接近 50% rowCount統計性驗證(1000 次取平均)FR-04-2
N=3 density=0.65統計性驗證FR-04-2
N=4 density=0.75統計性驗證FR-04-2
N≥5 density=0.90統計性驗證FR-04-2

§2.1.6 ComputeResults — 路徑計算與 bijection

測試案例說明AC/FR
所有 endCol 唯一(bijection)N 個起點 → N 個不同終點FR-04-4, NFR-03
無橫槓時 endCol = startCol直線走完FR-05-2
遇到右橫槓(segmentSet has ${row}:${col})向右移動路徑方向正確FR-05-2
遇到左橫槓(segmentSet has ${row}:${col-1})向左移動路徑方向正確FR-05-2
同 seed+N 呼叫兩次,results 完全一致determinismNFR-03
1000 次隨機 seed 驗證 bijection 不失敗(CI property-based test)大規模驗證NFR-03

§2.1.7 ValidateGameStart

測試案例說明AC/FR
N=2, W=1 → 通過最小合法邊界AC-H03-1
N=50, W=49 → 通過最大合法邊界AC-H03-1
N=1 → INSUFFICIENT_PLAYERSN < 2AC-H03-4
W=0 → INVALID_PRIZES_COUNTW < 1AC-H02-2
W=N → INVALID_PRIZES_COUNTW >= NAC-H02-2
W=null → PRIZES_NOT_SET未設定AC-H03-2

§2.2 packages/server

測試檔案位置: packages/server/src/tests/

§2.2.1 RoomRepository(mock Redis)

測試案例說明AC/FR
createRoom — 成功建立並設 TTL=24h正常路徑FR-01-1, FR-01-2
createRoom — Room Code 碰撞重試最多 10 次碰撞逻辑FR-01-2, AC-H01-4
createRoom — 重試超過 10 次拋出 ROOM_CODE_GENERATION_FAILED失敗路徑AC-H01-4
getRoom — 存在房間回傳完整物件正常路徑FR-01-3
getRoom — 不存在/已過期拋出 ROOM_NOT_FOUND邊界FR-02-6
addPlayer — 成功加入並廣播 ROOM_STATE正常路徑FR-02-1
addPlayer — 暱稱重複拋出 NICKNAME_TAKEN(含離線玩家)AC-P01-2, §6.4
addPlayer — 人數達 50 拋出 ROOM_FULLAC-P01-4, FR-02-3
addPlayer — 房間狀態非 waiting 拋出 ROOM_NOT_ACCEPTINGAC-P01-8, FR-02-6
kickPlayer — 成功移除並持久化 kickedPlayerIdsFR-11-2
kickPlayer — 非 waiting 狀態拋出 INVALID_STATEFR-11-3
kickPlayer — 踢自己拋出 CANNOT_KICK_HOSTAC-H07-3b
updateWinnerCount — waiting 狀態成功AC-H02-1
updateWinnerCount — 非 waiting 拋出 INVALID_STATEAC-H02-4
updateTitle — waiting 狀態成功AC-H01-5, FR-01-5
updateTitle — 非 waiting 拋出 TITLE_UPDATE_NOT_ALLOWED_IN_STATEAC-H01-5
INCR revealedCount 原子操作防 race conditionFR-13-1
TTL — finished 後設為 1hFR-01-2, EDD §9.1
TTL — 最後一人斷線設為 5minEDD §9.1

§2.2.2 RoomService — business logic

測試案例說明AC/FR
createRoom — 成功回傳 roomCode, playerId, tokenUS-H01, FR-01-4
joinRoom — 存入 localStorage 相關資訊(服務端邏輯)FR-02-1
reconnect — 帶 playerId 回傳 ROOM_STATE_FULLFR-10-1
reconnect — playerId 在 kickedPlayerIds 拒絕FR-10-1, AC-P06-2
reconnect — 同 playerId 新連線觸發 SESSION_REPLACEDFR-10-3
ghostPlayer(waiting) — 視為新玩家FR-10-2
ghostPlayer(running/revealing) — 回傳 ROOM_NOT_JOINABLEFR-10-2
playAgain — 剔除 isOnline=false 玩家FR-12-1, AC-H08-1
playAgain — W 越界重設為 nullAC-H08-2, FR-12-2
playAgain — 在線玩家 < 2 拋出 INSUFFICIENT_PLAYERSAC-H08-3
playAgain — 非 finished 狀態拋出 INVALID_STATEAC-H08-5
resetRoom — 任意狀態成功,不剔除離線玩家EDD §6.1

§2.2.3 GameService — state transitions

測試案例說明AC/FR
startGame — waiting→running,廣播 rowCount,不含 seedAC-H03-1, FR-04-6
startGame — N < 2 拋出 INSUFFICIENT_PLAYERSAC-H03-4
startGame — W 未設定拋出 PRIZES_NOT_SETAC-H03-2
startGame — W >= N 拋出 INVALID_PRIZES_COUNTAC-H02-2
startGame — 重複呼叫(已 running)拋出 INVALID_STATEAC-H03-6
beginReveal — running→revealing,原子生成 LadderDataAC-H04-1
beginReveal — 非 running 狀態拋出 INVALID_STATEAC-H04-5
revealNext — 廣播 REVEAL_INDEX,revealedCount +1AC-H04-2
revealNext — revealedCount === totalCount 時不廣播AC-H04-3
revealNext — 非 revealing 狀態拋出 INVALID_STATEAC-H04-6
revealAll — 廣播 REVEAL_ALL(ResultSlotPublic,省略 path)AC-H06-1
revealAll — 所有路徑已揭曉時不廣播AC-H06-3
revealAll — 非 revealing 拋出 INVALID_STATEAC-H06-4
endGame — revealing→finished,seed 首次公開AC-H04-4, FR-04-6
endGame — revealedCount < totalCount 拋出 END_GAME_REQUIRES_ALL_REVEALEDAC-H04-4b
endGame — 非 revealing 拋出 INVALID_STATEFR-09-3
setRevealMode(auto) — intervalSec 合法(1-300)啟動計時器AC-H05-1
setRevealMode(auto) — intervalSec 非整數拋出 INVALID_INTERVALAC-H05-3
setRevealMode(auto) — intervalSec > 300 拋出 INVALID_INTERVALAC-H05-3
setRevealMode(manual) — 停止自動計時器AC-H05-2
autoReveal timer — 全揭後自動停止(不廣播多餘事件)AC-H05-4
autoReveal + manual 同時觸發 — INCR 原子,revealedCount 只增 1§6.8

§2.2.4 WebSocket handler — message routing

測試案例說明AC/FR
非 JSON 訊息 → ERROR WS_INVALID_MSG,保持連線FR-03-4, EDD §11.1
未知 type → ERROR WS_UNKNOWN_TYPEEDD §11.1
非 host 傳送 host-only 訊息 → ERROR PLAYER_NOT_HOSTFR-03-2
maxPayload 64KB 超過 → 伺服器關閉連線FR-03-5, EDD §6.1
速率超限 60 msg/min → close 4029EDD §10.1
JWT 驗證失敗(Upgrade 階段)→ 403EDD §8.2
kickedPlayerId 在 Upgrade 階段 → close 4003FR-11-2, EDD §8.2
Origin 不在白名單 → 403EDD §10.3

§2.2.5 Security — seed 防洩漏

測試案例說明AC/FR
waiting/running 狀態 ROOM_STATE 不含 seedNFR-05, FR-04-6
revealing 狀態 ROOM_STATE_FULL 使用 LadderDataPublic(省略 seed)NFR-05, EDD §10.2
finished 狀態 ROOM_STATE 含 seedAC-H04-4
GET /api/rooms/:code 不暴露 hostId(RoomSummaryPayload)EDD §10.1

§2.3 packages/client

測試檔案位置: packages/client/src/tests/

§2.3.1 LocalStorageService

測試案例說明AC/FR
saveNickname — 正確寫入 ladder_last_nicknameFR-08-1, AC-P01-6
loadNickname — 讀取已儲存暱稱FR-08-2
loadNickname — 無暱稱時回傳 nullFR-08-2
savePlayerId — 寫入 UUID v4FR-08-3
loadPlayerId — 讀取 playerIdFR-10-1
clearPlayerId — 清除 playerId(踢除路徑)FR-08-4, AC-P06-1
clearPlayerId — 連線替換路徑同樣清除FR-08-4

§2.3.2 Canvas renderer(mock canvas 2D context)

測試案例說明AC/FR
drawRails — 繪製 N 條垂直線FR-06-1
drawRungs — 依 segments[] 繪製橫槓FR-06-2
drawRevealedPath(自己)— 高亮色 + alpha=1.0FR-06-3
drawRevealedPath(他人)— 對應色 + alpha=0.6FR-06-3, FR-06-5
drawUnrevealedPath — 灰色虛線FR-06-3
drawWinnerStar — shadowBlur=10 金色光暈FR-06-5, FR-07-3
colorFromIndex — 50 個不同顏色,無重複FR-06-5
colorFromIndexDim — 淡化版顏色FR-06-5
REVEAL_ALL 後 2 秒超時 → 跳至終止幀FR-06-6, AC-H06-1

§2.3.3 WS client 重連策略

測試案例說明AC/FR
指數退避序列:1/2/4/8/30sEDD §6.2
5 次失敗後停止重連EDD §6.2
SESSION_REPLACED → Modal 提示,跳首頁EDD §11.2
PLAYER_KICKED → 清除 playerId,顯示通知,關閉 WSAC-P06-1, FR-08-4

§2.3.4 URL 與 localStorage 預填邏輯

測試案例說明AC/FR
URL ?room=AB3K7X → 自動預填 Room Code 欄位AC-P01-5, FR-14-2
localStorage ladder_last_nickname 有值 → 自動預填暱稱AC-P01-6, FR-08-2
Clipboard API 可用 → 寫入邀請 URL,按鈕顯示「已複製!」1.5sAC-H09-2, FR-14-3
Clipboard API 不可用 → 顯示 fallback <input> 並全選AC-H09-3, FR-14-3

§3 Integration Test Plan

測試框架: Vitest + testcontainers(真實 Redis Docker 容器)

測試檔案位置: packages/server/src/tests/integration/

§3.1 REST API Integration

POST /api/rooms → GET /api/rooms/:code 完整 round-trip

測試案例步驟驗證點AC/FR
建立房間成功POST /api/rooms → 201roomCode 6 碼合法字元集;token JWT 有效AC-H01-1
建立後查詢房間GET /api/rooms/:code → 200status=waiting,playerCount=1AC-H01-2
加入房間POST /api/rooms/:code/players → 201playerId UUID v4;room.players 長度增加AC-P01-1
踢出玩家DELETE /api/rooms/:code/players/:id(host token)→ 204後續 GET 確認玩家已移除AC-H07-1
開始遊戲POST /api/rooms/:code/game/start → 200status=running;rowCount 符合公式AC-H03-1
揭示(next mode)POST /api/rooms/:code/game/reveal {mode:"next"} → 200revealedCount +1AC-H04-2
揭示(all mode)POST /api/rooms/:code/game/reveal {mode:"all"} → 200ladder 為 LadderDataPublic(無 seed)AC-H06-1
結束本局POST /api/rooms/:code/game/end → 200status=finished;含 seedAC-H04-4
再玩一局POST /api/rooms/:code/game/play-again → 200status=waiting;kickedPlayerIds=[]AC-H08-1
重置房間POST /api/rooms/:code/game/reset → 200status=waiting;含離線玩家EDD §2.9

錯誤情境 REST Integration

測試案例請求預期回應AC/FR
查詢不存在房間GET /api/rooms/XXXXXX404 ROOM_NOT_FOUNDAC-P01-7
暱稱重複加入POST players x2 同暱稱409 NICKNAME_TAKENAC-P01-2
房間滿員(50人)加入POST players 第 51 次409 ROOM_FULLAC-P01-4
非 host 呼叫 startPOST game/start(player token)403 PLAYER_NOT_HOSTEDD §8.2
JWT 過期呼叫 host 操作模擬過期 token401 AUTH_TOKEN_EXPIREDAC-H08-4
N < 2 開始遊戲POST game/start(只有 host)400 INSUFFICIENT_PLAYERSAC-H03-4
W 未設定開始遊戲POST game/start(winnerCount=null)400 PRIZES_NOT_SETAC-H03-2
非 finished 狀態 play-againPOST game/play-again(running)409 INVALID_STATEAC-H08-5
endGame 尚有未揭曉POST game/end(revealedCount < N)409 END_GAME_REQUIRES_ALL_REVEALEDAC-H04-4b

Redis 原子操作 Integration

測試案例說明FR
WATCH/MULTI/EXEC 並發保護兩個同時 START_GAME 請求,只有一個成功§6.8
INCR revealedCount 並發兩個同時 REVEAL_NEXT,revealedCount 只增 1§6.8
Redis SETNX 唯一 Room Code模擬 10 次碰撞 → ROOM_CODE_GENERATION_FAILEDAC-H01-4
TTL 設定正確(waiting=24h, finished=1h)PTTL 驗證EDD §9.1

§3.2 WebSocket Integration

連線 → JOIN_ROOM → ROOM_UPDATE 流程

測試案例步驟驗證點AC/FR
WS 連線建立後收到 ROOM_STATE_FULL1.建立房間 2.WS connecttype=ROOM_STATE_FULL;selfPlayerId 正確EDD §6.2
玩家加入後所有連線收到 ROOM_STATE1.Host WS 2.Player REST join 3.Player WS兩個 client 均收到 ROOM_STATE;players.length=2AC-P02-1
玩家斷線後收到 ROOM_STATE(isOnline=false)WS 斷線其他 client 在 2s 內收到更新AC-P02-2
玩家重連後收到 ROOM_STATE_FULL帶 playerId 重連isOnline=true;房間狀態快照正確AC-P05-1

START_GAME → GAME_STARTED 流程

測試案例步驟驗證點AC/FR
Host START_GAME → 所有 client 收到 ROOM_STATE(running)2 client WSstatus=running;rowCount 符合公式;不含 seedAC-H03-1
rowCount 三邊界值自動驗證(N=3,10,21)E2E 建立 3/10/21 人房rowCount=20/30/60AC-H03-5

REVEAL_ONE → REVEAL_RESULT 流程

測試案例步驟驗證點AC/FR
BEGIN_REVEAL → ROOM_STATE(revealing)Host WSstatus=revealing;1s 內完成AC-H04-1
REVEAL_NEXT → REVEAL_INDEX 廣播Host WS REVEAL_NEXT所有 client 收到 REVEAL_INDEX;path+result 正確AC-H04-2
REVEAL_ALL_TRIGGER → REVEAL_ALL 廣播Host WS REVEAL_ALL_TRIGGERResultSlotPublic(無 path);payload < 64KBAC-H06-1
END_GAME → ROOM_STATE(finished) + seed 公開全揭後 END_GAMEstatus=finished;seed 首次出現在 payloadAC-H04-4

斷線/重連 WebSocket Integration

測試案例說明AC/FR
revealing 狀態重連 → 靜態已揭曉結果(不重播動畫)模擬斷線後 reconnectROOM_STATE_FULL 含已揭曉結果AC-P05-2
同 playerId 重複 WS 連線 → 舊連線收到 SESSION_REPLACED兩個 WS 連線同一 playerId舊連線 SESSION_REPLACED;新連線正常FR-10-3
kickedPlayerId 重連 → close 4003踢除後嘗試重連WS close code=4003AC-P06-2, FR-11-2
Host 斷線後 Player 保持連線Host WS 強制關閉Player WS 60s 內不中斷§6.6

§4 E2E Test Plan(Playwright)

測試框架: Playwright 1.x(Headless Chrome)

測試環境: Local(Docker Compose 或 K8s Rancher Desktop)

測試檔案位置: packages/e2e/tests/(或 packages/server/e2e/

§4.1 完整遊戲流程(2 個玩家,Happy Path P0)

// 對應所有 P0 User Story 的核心流程
test('complete game flow: 2 players', async ({ browser }) => {
  // Arrange: 開啟兩個獨立 browser context
  const hostCtx = await browser.newContext();
  const playerCtx = await browser.newContext();
  const hostPage = await hostCtx.newPage();
  const playerPage = await playerCtx.newPage();

  // Act + Assert: 依序驗證每個 Step
});
Step驗證點AC/FR
1. Host 建立房間POST /api/rooms 201;頁面顯示 6 碼 Room CodeAC-H01-1
2. Host 等待大廳顯示邀請連結頁面含「複製邀請連結」按鈕AC-H09-1
3. Player 以邀請 URL 加入URL ?room=CODE 自動預填;1.5s 內進入等待畫面AC-P01-5
4. Host 看到玩家列表更新2s 內玩家列表出現 Player 暱稱AC-P02-1
5. Host 設定 winnerCount=1廣播 ROOM_STATE;winnerCount=1AC-H02-1
6. Host 點擊「開始遊戲」ROOM_STATE(running);rowCount=20(N=2)AC-H03-1
7. Host 點擊「開始揭曉」ROOM_STATE(revealing);1s 內完成AC-H04-1
8. Host 點擊「下一位」× 2兩個 REVEAL_INDEX 廣播;Canvas 動畫播放AC-H04-2
9. Player 看到自己路徑高亮Canvas 高亮色顯示AC-P03-1
10. Host 點擊「結束本局」ROOM_STATE(finished);seed 公開;結果頁面AC-H04-4
11. Player 看到中獎/未中獎文字文字與 server result 一致AC-P04-1
12. Host 點擊「再玩一局」ROOM_STATE(waiting);kickedPlayerIds 清空AC-H08-1

§4.2 一鍵全揭流程(P0)

Step驗證點AC/FR
REVEAL_ALL_TRIGGER 後 2s 內所有動畫完成計時驗證;若超時 → 跳至終止幀AC-H06-1, FR-06-6
REVEAL_ALL 後 Host 按「結束本局」→ finished確認 seed 公開AC-H06-2

§4.3 踢除玩家流程(P1)

Step驗證點AC/FR
Host 踢除 PlayerPlayer 看到「你已被主持人移出房間」;WS 關閉AC-P06-1
被踢 Player 嘗試重連close 4003;顯示「你已被移出此房間」AC-P06-2

§4.4 斷線重連流程(P1)

Step驗證點AC/FR
遊戲 running 狀態 Player 斷線後重連3s 內(DOMContentLoaded → 狀態渲染)恢復AC-P05-1
revealing 狀態重連 → 靜態已揭曉結果不重播動畫;已揭曉結果正確顯示AC-P05-2

§4.5 50 人房間上限(P0)

Step驗證點AC/FR
嘗試加入第 51 個玩家顯示「房間已滿,無法加入」AC-P01-4

§4.6 路徑揭示動畫完成確認

Step驗證點AC/FR
REVEAL_INDEX 後等待動畫完成 selectorCanvas animate 結束 selector 可見AC-P03-1
動畫完成後底部結果槽顯示結果DOM 元素持久顯示AC-P03-2

§4.7 自動揭曉模式(P1)

Step驗證點AC/FR
SET_REVEAL_MODE(auto, intervalSec=1) → 自動廣播每秒收到一個 REVEAL_INDEXAC-H05-1
切換回手動模式 → 自動停止3s 內不再有新 REVEAL_INDEXAC-H05-2

§4.8 多瀏覽器相容性(P1)


§5 Performance Test Plan

§5.1 WS 並發壓測(k6)

工具: k6(WebSocket 支援)

目標環境: Local K8s(Rancher Desktop)或 Staging

測試腳本: k6/ws-load-test.js

// 目標:100 房間 × 50 人 × 完整遊戲流程
export const options = {
  scenarios: {
    ws_game_load: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '1m', target: 5000 },  // 5,000 WS 連線
        { duration: '3m', target: 5000 },  // 穩定壓測
        { duration: '1m', target: 0 },
      ],
    },
  },
  thresholds: {
    'ws_connecting': ['p(95)<1500'],    // WS 握手 P95 < 1.5s
    'ws_msgs_received': ['rate>0.995'], // 訊息接收成功率 > 99.5%
    'http_req_failed': ['rate<0.005'],  // HTTP 失敗率 < 0.5%
  },
};
指標目標值量測方式
並發 WS 連線數5,000(100房間 × 50人)k6 ws_sessions
建立房間成功率> 99.5%k6 http_req_failed
玩家加入成功率> 99%k6 http_req_failed
WS 廣播延遲(P95)< 2s(伺服器發出→客戶端完成更新)k6 ws_session_duration
WS 握手延遲(P99)< 1.5sk6 ws_connecting
斷線重連時間(P95)< 3sk6 自訂 metric
Redis 操作< 500 ops/sk6 + Redis INFO

§5.2 HTTP API 壓測(Autocannon)

工具: Autocannon

目標: POST /api/roomsPOST /api/rooms/:code/players

# 建立房間壓測
autocannon -c 50 -d 30 -m POST -H 'Content-Type:application/json' \
  -b '{"hostNickname":"Host","winnerCount":1}' \
  http://ladder.local/api/rooms

# 目標:P99 < 2s,成功率 > 99.5%

§5.3 前端效能壓測(Lighthouse CI)

工具: Lighthouse CI(@lhci/cli

觸發時機: 每次 PR(GitHub Actions)

設定檔: lighthouserc.js

module.exports = {
  ci: {
    collect: { url: ['http://ladder.local/'], numberOfRuns: 3 },
    assert: {
      assertions: {
        'first-contentful-paint': ['warn', { maxNumericValue: 1500 }],
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
        'total-blocking-time': ['warn', { maxNumericValue: 200 }],
      },
    },
  },
};
指標目標值觸發 CI 阻斷
FCP(Slow 4G)< 1.5s
LCP(Slow 4G)< 2.5s
CLS< 0.1
TBT< 200ms警告
JS bundle 首頁(gzip)< 80KB
JS bundle 遊戲頁(gzip)< 150KB
CSS(gzip)< 30KB警告

§5.4 Canvas FPS 壓測

測試案例環境目標量測方式
50 人滿員房間,揭示動畫 FPS(桌機)Chrome 1080p≥ 30fpsChrome DevTools Performance → requestAnimationFrame timing
50 人滿員房間,揭示動畫 FPS(手機)Chrome DevTools Moto G4 throttling≥ 24fps(10s 平均)Chrome DevTools Performance

§6 UAT Scenarios

每個 P0 User Story 對應至少 1 個 UAT 情境,由 QA / Product 人工驗證。

UAT-ID對應 US情境標題驗收人
UAT-H01US-H01主持人建立房間並取得 6 碼邀請連結QA
UAT-H02US-H02主持人設定中獎名額(邊界值 W=1, W=N-1)QA
UAT-H03US-H03主持人開始遊戲,所有客戶端一致顯示梯子QA + PM
UAT-H04aUS-H04主持人手動逐步揭曉(完整流程,N=5)QA
UAT-H04bUS-H04主持人在揭曉中途觸發結束(預期失敗提示)QA
UAT-H05US-H05主持人設定自動揭曉 T=3s,切換回手動QA
UAT-H06US-H06主持人一鍵揭曉,2s 內動畫完成QA
UAT-H07US-H07主持人踢除玩家,被踢者看到通知QA
UAT-H08US-H08主持人再玩一局,離線玩家自動剔除QA
UAT-H09US-H09主持人複製邀請連結(Clipboard + Fallback)QA
UAT-P01aUS-P01玩家透過邀請 URL 加入(自動預填 Room Code)QA
UAT-P01bUS-P01玩家使用上次暱稱(localStorage 預填)QA
UAT-P01cUS-P01玩家加入已滿房間(顯示房間已滿)QA
UAT-P02US-P02多玩家同時在線,玩家列表即時更新(< 2s)QA
UAT-P03US-P03玩家觀看自己路徑動畫(高亮 + FPS 目視確認)QA
UAT-P04US-P04玩家確認中獎/未中獎結果與主持人一致QA + PM
UAT-P05aUS-P05玩家斷線後重連恢復(waiting 狀態)QA
UAT-P05bUS-P05玩家在 revealing 狀態斷線後重連(靜態結果,不重播)QA
UAT-P06US-P06被踢玩家看到通知並無法重新加入QA

UAT 通過標準: 所有 P0 UAT 全數通過,P1 UAT 通過率 ≥ 90%


§7 Test Coverage Targets

套件層次目標覆蓋率工具
packages/sharedUnit≥ 90%Vitest v8 coverage
packages/server(application/domain)Unit≥ 80%Vitest v8 coverage
packages/client(非 Canvas DOM)Unit≥ 70%Vitest v8 coverage
REST API 端點Integration100%(所有 path)Vitest + testcontainers
WS 訊息類型Integration100%(所有 WsMsgType)Vitest + testcontainers
P0 User Story Happy PathE2E100%Playwright
P0 User Story Error PathE2E≥ 80%Playwright
P1 User Story Happy PathE2E≥ 70%Playwright

§7.1 CI 覆蓋率閘門設定

vitest.config.ts:

export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      thresholds: {
        // packages/shared
        lines: 90,
        functions: 90,
        branches: 85,
        statements: 90,
      },
    },
  },
});

GitHub Actions 閘門:

- name: Unit + Integration Test with Coverage
  run: npx vitest --coverage --reporter=json
- name: Assert coverage >= 80% (server)
  run: node scripts/check-coverage.js packages/server 80
- name: Assert coverage >= 90% (shared)
  run: node scripts/check-coverage.js packages/shared 90

§8 Migration Baseline(重要!)

§8.1 現有測試基準

截至 2026-04-21,現有 Vitest 測試:171 個全數通過(來源:EDD §12.4)

參考快照:docs/MIGRATION_CODE_SNAPSHOT.md

遷移後必須維持以下條件:

  1. 所有 171 個現有測試繼續通過(不得因重構導致退化)
  2. 新增測試不得降低現有覆蓋率
  3. 遷移步驟:先跑 npx vitest run 確認基準 171 通過 → 實施變更 → 再跑確認仍 171+ 通過

§8.2 Migration 測試執行順序

# Step 1: 確認遷移前基準
npx vitest run --reporter=json | grep "Tests " # 應顯示 171 passed

# Step 2: 執行遷移(程式碼變更)

# Step 3: 確認遷移後基準維持
npx vitest run --reporter=json | grep "Tests " # 應顯示 ≥ 171 passed,0 failed

# Step 4: 新增測試(本 Test Plan 定義的新 cases)
npx vitest run --coverage

§8.3 不可破壞的現有測試類別(依 MIGRATION_CODE_SNAPSHOT)


§9 Test Environments

§9.1 Local — Unit & Integration

項目設定
RuntimeNode.js 20 LTS
測試框架Vitest 1.x
Redistestcontainers(真實 Docker Redis 6+ 容器)
執行指令npm run test:unitnpm run test:integration
CI 環境變數NODE_ENV=testREDIS_URL=redis://localhost:6379

§9.2 Local — E2E(Playwright + K8s)

項目設定
執行環境Rancher Desktop(containerd),http://ladder.local
瀏覽器Chromium(Headless)、Firefox、WebKit
啟動指令./scripts/dev-k8s.sh up && npx playwright test
截圖路徑playwright-report/screenshots/
影片錄製僅 CI 失敗時保留
Timeout30s per test(WS 操作含重試)

§9.3 Local — Performance(k6)

項目設定
工具k6(官方 Docker image:grafana/k6:latest
目標環境http://ladder.local(K8s Rancher Desktop)
執行指令docker run --rm -i grafana/k6 run - < k6/ws-load-test.js
結果輸出k6 summary JSON + Prometheus 指標(prom-client)

§9.4 CI(GitHub Actions)

階段工具觸發時機
lint + typecheckESLint + tsc --noEmit每次 PR
npm auditnpm audit --audit-level=high每次 PR
unit-testVitest(節點環境)每次 PR
integration-testVitest + testcontainers每次 PR
coverage-gateVitest coverage ≥ 80%/90%每次 PR(阻斷)
buildnpm run build + Docker multi-stage每次 PR
e2ePlaywright(Docker Compose)每次 PR
lighthouse-ci@lhci/cli每次 PR(阻斷 FCP/LCP/CLS)
k6 壓測k6 WebSocket(100房間×50人)週期性(非每次 PR,待 OQ-08 確認)

TEST_PLAN 版本:v1.0

生成時間:2026-04-21(devsop-autodev STEP-14)

基於 PRD v1.0 + EDD v2.0 + API v2.1