/** * 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: SKELETON(M9-12 之前的占位、Reviewer 可決定是否啟用) */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import type { FirmwareActiveTask } from '@/types/device'; // ────────────────────────────────────────────────────────────────────── // Test fixtures // ────────────────────────────────────────────────────────────────────── function makeActiveTask(overrides: Partial = {}): 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 void)[]>; } { const subscribers = new Map 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 binding:window.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) // - chip(KL520 / KL720) // - direction(upgrade / downgrade) // - stage(flashing / 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-12:force-quit modal 第二層 FORCE 確認字串 input 嚴格 === 比對'); it.todo('M9-12:第二層輸入「force」小寫不通過(防誤觸)'); it.todo('M9-12:「繼續等待」按鈕關閉 force-quit modal、不呼 ConfirmForceClose'); it.todo('M9-12:firmware 升級完成後 modal 自動關閉'); }); // ────────────────────────────────────────────────────────────────────── // 補充:對 Go 端 close-guard 測試的 cross-reference // ────────────────────────────────────────────────────────────────────── describe('Go 端 close-guard test 已涵蓋(cross-ref)', () => { it('cross-ref:visiona-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 true(prevent 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 自動化腳本對照表) }); });