/** * PairView 單元測試(Fix-F2 / Fix-F3 涵蓋) * * 驗證重點: * 1. Fix-F2:Esc 鍵 dispatch agent:switch-tab → status * 2. Fix-F2:submitting=true 時 Esc 不觸發 * 3. Fix-F2:unmount 後不再響應 keydown(避免 memory leak / 跨 view 殘留) * 4. Fix-F3:點擊「前往雲端」改用 agentAPI.openURL(不直接呼叫 window.open) * * 不測試: * - usePair 的 submitting / lastError 行為(hook 自己有測試) * - TokenInput 內部驗證(token-input.test.tsx 負責) */ import { act, fireEvent, render, screen } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { LocaleProvider } from "@/lib/i18n/context"; // usePair 用 ref 控制:每個 test 改 currentSubmitting 即可影響 hook 回傳 let currentSubmitting = false; const pairFn = vi.fn().mockResolvedValue(undefined); const resetFn = vi.fn(); vi.mock("@/hooks/use-pair", () => ({ usePair: () => ({ pair: pairFn, submitting: currentSubmitting, lastError: null, reset: resetFn, }), })); const openURLMock = vi.fn(); vi.mock("@/lib/agent-api", () => ({ agentAPI: { openURL: (url: string) => openURLMock(url), }, })); import { AGENT_SWITCH_TAB_EVENT } from "./status-view"; import { PairView } from "./pair-view"; function renderPair() { return render( , ); } describe(" — Esc 取消(Fix-F2)", () => { let dispatchSpy: ReturnType; beforeEach(() => { currentSubmitting = false; pairFn.mockClear(); resetFn.mockClear(); openURLMock.mockClear(); dispatchSpy = vi.spyOn(window, "dispatchEvent"); }); afterEach(() => { dispatchSpy.mockRestore(); }); it("按 Esc 會 dispatch agent:switch-tab → status", () => { renderPair(); act(() => { window.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" })); }); const switchCall = dispatchSpy.mock.calls.find(([ev]) => { return ev instanceof CustomEvent && ev.type === AGENT_SWITCH_TAB_EVENT; }); expect(switchCall).toBeDefined(); const [event] = switchCall as [CustomEvent<{ value: string }>]; expect(event.detail.value).toBe("status"); }); it("submitting=true 時按 Esc 不 dispatch(避免打斷送出)", () => { currentSubmitting = true; renderPair(); act(() => { window.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" })); }); const switchCall = dispatchSpy.mock.calls.find(([ev]) => { return ev instanceof CustomEvent && ev.type === AGENT_SWITCH_TAB_EVENT; }); expect(switchCall).toBeUndefined(); }); it("unmount 後 Esc 不再響應(解綁 listener)", () => { const { unmount } = renderPair(); unmount(); dispatchSpy.mockClear(); act(() => { window.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" })); }); const switchCall = dispatchSpy.mock.calls.find(([ev]) => { return ev instanceof CustomEvent && ev.type === AGENT_SWITCH_TAB_EVENT; }); expect(switchCall).toBeUndefined(); }); it("非 Escape 鍵不觸發切 tab", () => { renderPair(); act(() => { window.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" })); window.dispatchEvent(new KeyboardEvent("keydown", { key: "Tab" })); }); const switchCall = dispatchSpy.mock.calls.find(([ev]) => { return ev instanceof CustomEvent && ev.type === AGENT_SWITCH_TAB_EVENT; }); expect(switchCall).toBeUndefined(); }); }); describe(" — 雲端連結改用 agentAPI.openURL(Fix-F3)", () => { beforeEach(() => { currentSubmitting = false; openURLMock.mockClear(); }); it("點擊「前往雲端」連結會呼叫 agentAPI.openURL", () => { renderPair(); fireEvent.click(screen.getByTestId("pair-link-cloud")); expect(openURLMock).toHaveBeenCalledOnce(); expect(openURLMock).toHaveBeenCalledWith( expect.stringMatching(/^https:\/\/visionA\.cloud\/devices\/pair$/i), ); }); });