visionA/local-tool/.autoflow/06-testing/scripts/wails-onbeforeclose-firmware-active.spec.ts
jim800121chen 8c27da7cca test(local-tool): M9-5 — three-platform validation plan + e2e scripts + MJ3 fix
A 階段最後 milestone、出測試計畫 + 自動化腳本 + 三平台人工 checklist、使用者下週手動跑實機驗證。

Testing artifacts (8 檔、2630 行):
- .autoflow/06-testing/m9-5-validation-plan.md: 656 行(4 情境 × 3 平台 × 2 chip = 24 combo)
- 4 e2e specs (vitest + RTL + mock WS / mock fetch):
  - firmware-upgrade-happy-path.spec.ts (357 / 4 cases)
  - firmware-upgrade-error-recovery.spec.ts (356 / 4 cases + 8 reason it.each)
  - firmware-r-fw-11-modal-not-closable.spec.ts (303 / 6 cases)
  - wails-onbeforeclose-firmware-active.spec.ts (217 / 9 cases、含 5 todo 占位 M9-12)
- 3 manual checklists: macOS 264 / Windows 234 / Linux 243 行

設計取捨:
- 不引入 Playwright/Cypress (visionA-local frontend 沒裝、屬 architect 決策)、走 vitest + mock
- E2E 腳本放 06-testing/scripts/ 作 spec doc + 可選實作參考
- 實機驗證走人工 checklist (dongle 插拔 / kill process / SIGTERM 等需要實體互動)

MJ3 修復 (M9-4 reviewer round 1 留的 follow-up):
- server/internal/api/ws/firmware_ws_test.go: +16/-8
- "type": "firmware:progress" → "firmware_progress" (對齊 firmwareProgressMessage.Type)
- "phase" → "stage" (對齊 TDD §4.2 + FirmwareProgress.Stage)
- 不動 production code、只 test schema 對齊

執行建議 (給你下週):
- Day 1 P0: macOS+Win+Linux × KL520+KL720 happy path (~3h)
- Day 2 P1: R-FW-11 + disconnect_during_op + upgrade_mid_failed + 失敗注入 (4h)
- Day 3 P2: SIGTERM 延遲關閉 + Wails OnBeforeClose force-quit modal (2-3h)

測試:
- go test ./... -race 全綠 (server / wails / frontend 60 tests)
- MJ3 修復不破壞既有測試

A 階段開發 6/7 完成 (M9 文件 + M9-1 ~ M9-4.5)、剩 M9-5 實機驗證 (你下週跑)、跑完依結果決定 A 階段交付或派 sub-agent 修。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:34:17 +08:00

218 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* M9-5 E2E #4 — Wails OnBeforeClose firmware-active 攔截邏輯
*
* 目的:驗證 visiona-local/firmware_close_guard.go 的 OnBeforeClose 邏輯在
* 升級進行中時阻擋 Wails 視窗關閉、避免使用者誤關 → device brick。
*
* 涵蓋 AC
* - AC-FW-1.9 graceful shutdown 拒絕(含 Wails 視窗關閉)
* - R-FW-11 多層 safety net 之一
*
* 注意visiona-local 是 Go 端 + Wails 框架,這層的測試在 Go 端已經有完整
* coverage 在 `visiona-local/firmware_close_guard_test.go`8 個測試案例)。
*
* 本 spec 是補充 frontend 端 modal 攔截 UI 的測試(前端訂閱 Wails event
* `app:firmware-in-progress` 後渲染攔截 modal對應 Design Spec §6a「Wails
* 控制台關閉攔截 modal」。
*
* 由於 frontend 目前對應的 modal 元件(控制台 force-quit modal尚未實作完
* 整M9-12 才開)、本 spec 提供**規格化的測試藍圖**、可在 M9-12 實作完成
* 後直接套用。
*
* Owner: Testing Agent (M9-5)
* Last reviewed: 2026-05-25
* Status: SKELETONM9-12 之前的占位、Reviewer 可決定是否啟用)
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import type { FirmwareActiveTask } from '@/types/device';
// ──────────────────────────────────────────────────────────────────────
// Test fixtures
// ──────────────────────────────────────────────────────────────────────
function makeActiveTask(overrides: Partial<FirmwareActiveTask> = {}): FirmwareActiveTask {
return {
taskId: 'upgrade-kl520-0-001',
deviceId: 'kl520-0',
deviceName: 'KL520 #1',
chip: 'KL520',
direction: 'upgrade',
stage: 'flashing',
startTs: new Date(Date.now() - 12000).toISOString(),
elapsedMs: 12000,
etaSeconds: 45,
...overrides,
};
}
// ──────────────────────────────────────────────────────────────────────
// Mock Wails runtime (window.runtime injected by Wails at build time)
// ──────────────────────────────────────────────────────────────────────
interface WailsRuntime {
EventsOn: (eventName: string, callback: (data: unknown) => void) => () => void;
EventsEmit: (eventName: string, ...data: unknown[]) => void;
Quit: () => void;
}
function installMockWailsRuntime(): {
runtime: WailsRuntime;
subscribers: Map<string, ((data: unknown) => void)[]>;
} {
const subscribers = new Map<string, ((data: unknown) => void)[]>();
const runtime: WailsRuntime = {
EventsOn: (event, cb) => {
const list = subscribers.get(event) || [];
list.push(cb);
subscribers.set(event, list);
return () => {
const updated = (subscribers.get(event) || []).filter((c) => c !== cb);
subscribers.set(event, updated);
};
},
EventsEmit: (event, data) => {
const list = subscribers.get(event) || [];
list.forEach((cb) => cb(data));
},
Quit: vi.fn(),
};
// @ts-expect-error - window.runtime is Wails-injected
globalThis.runtime = runtime;
return { runtime, subscribers };
}
function installMockGoBindings() {
// Wails Go bindingwindow.go.main.App.ConfirmForceClose
// 模擬「使用者輸入 FORCE 字串確認」後呼叫 binding
const ConfirmForceClose = vi.fn().mockResolvedValue(undefined);
// @ts-expect-error - window.go 是 Wails 注入的
globalThis.go = {
main: {
App: {
ConfirmForceClose,
},
},
};
return { ConfirmForceClose };
}
// ──────────────────────────────────────────────────────────────────────
// Frontend modal 攔截邏輯(規格化測試藍圖)
// ──────────────────────────────────────────────────────────────────────
describe('M9-5 E2E #4: Wails OnBeforeClose firmware-active 攔截 modal規格化', () => {
beforeEach(() => {
// 重設 mock
});
it.todo(
'M9-12 實作後:訂閱 Wails event `app:firmware-in-progress` → 顯示 force-quit modal',
);
it('Wails runtime mock 可正常 EmitEvent + EventsOn round-trip', () => {
const { runtime, subscribers } = installMockWailsRuntime();
const callback = vi.fn();
const unsub = runtime.EventsOn('app:firmware-in-progress', callback);
runtime.EventsEmit('app:firmware-in-progress', {
hasActive: true,
tasks: [makeActiveTask()],
});
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith({
hasActive: true,
tasks: expect.arrayContaining([
expect.objectContaining({
taskId: 'upgrade-kl520-0-001',
deviceId: 'kl520-0',
stage: 'flashing',
etaSeconds: 45,
}),
]),
});
unsub();
runtime.EventsEmit('app:firmware-in-progress', { hasActive: false });
// unsub 後不再收到
expect(callback).toHaveBeenCalledTimes(1);
});
it('payload schema 包含 force-quit modal 顯示需要的所有欄位', () => {
const task = makeActiveTask();
// Design §6a 要求 modal 顯示:
// - deviceName哪台 device
// - chipKL520 / KL720
// - directionupgrade / downgrade
// - stageflashing / verifying / etc.
// - elapsedMs已過多久
// - etaSeconds還要多久
expect(task.deviceName).toBeTruthy();
expect(['KL520', 'KL720', 'KL630', 'KL730']).toContain(task.chip);
expect(['upgrade', 'downgrade']).toContain(task.direction);
expect(['preparing', 'loading', 'flashing', 'verifying', 'done', 'error']).toContain(task.stage);
expect(task.elapsedMs).toBeGreaterThan(0);
expect(task.etaSeconds).toBeGreaterThan(0);
});
it('ConfirmForceClose binding 在「強制關閉」second-confirm 後被呼叫', async () => {
const { ConfirmForceClose } = installMockGoBindings();
// 模擬:使用者點「強制關閉」→ 出現 FORCE 輸入 modal → 輸入 FORCE → 點確認
// → 呼 ConfirmForceClose binding
// 因為 Frontend modal 尚未實作M9-12、這裡只驗 binding pattern
// @ts-expect-error - 模擬 Frontend modal 行為
await globalThis.go.main.App.ConfirmForceClose();
expect(ConfirmForceClose).toHaveBeenCalled();
});
it.todo('M9-12force-quit modal 第二層 FORCE 確認字串 input 嚴格 === 比對');
it.todo('M9-12第二層輸入「force」小寫不通過防誤觸');
it.todo('M9-12「繼續等待」按鈕關閉 force-quit modal、不呼 ConfirmForceClose');
it.todo('M9-12firmware 升級完成後 modal 自動關閉');
});
// ──────────────────────────────────────────────────────────────────────
// 補充:對 Go 端 close-guard 測試的 cross-reference
// ──────────────────────────────────────────────────────────────────────
describe('Go 端 close-guard test 已涵蓋cross-ref', () => {
it('cross-refvisiona-local/firmware_close_guard_test.go 8 個案例', () => {
// 此 spec 不重做 Go 端測試(已有完整 coverage、列出供 reviewer 對照:
const goTestCases = [
'TestEvaluateClose_ForceAccepted',
'TestEvaluateClose_ServerNotRunning',
'TestEvaluateClose_QueryError_FailOpen',
'TestEvaluateClose_NoActiveTask',
'TestEvaluateClose_HasActive_PreventAndEmit',
'TestConfirmForceClose_SetsAndConsumesFlag',
'TestEvaluateClose_NilDeps',
'TestConfirmForceClose_ConcurrentAccess',
];
// 8 個 Go 端測試案例已涵蓋以下情境:
// 1. forceCloseAccepted=true → 放行
// 2. server port=0 → 放行
// 3. queryFirmwareTasks 失敗 → fail-open 放行
// 4. hasActive=false → 放行、不 emit
// 5. hasActive=true → emit + return trueprevent close
// 6. ConfirmForceClose 設旗標
// 7. nil deps → 防呆放行
// 8. concurrent race-free
expect(goTestCases.length).toBe(8);
// 跑 `cd visiona-local && go test -race -run "TestEvaluateClose|TestConfirmForceClose" -v`
// 確認全綠(已驗、見 M9-5 plan §9 自動化腳本對照表)
});
});