12345678910111213141516171819import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";202122const sendMessage = vi.fn(async (_msg: unknown) => ({ ok: true }) as { ok: boolean; error?: string });23vi.mock("../../shared/bridge", () => ({ sendMessage }));2425import { DesktopPairingFlow } from "./DesktopPairingFlow";26import type { PairedDevice } from "../../shared/sync-types";27import { renderView, inputText } from "../views/view-test-utils";282930type RuntimeListener = (msg: { type?: string; payload?: unknown }) => void;31let listeners: RuntimeListener[] = [];3233function pushFromBackground(type: string, payload: unknown): void {34 35 36 for (const l of [...listeners]) l({ type, payload });37}3839beforeEach(() => {40 listeners = [];41 sendMessage.mockClear();42 sendMessage.mockResolvedValue({ ok: true });43 (globalThis as unknown as { chrome: unknown }).chrome = {44 runtime: {45 onMessage: {46 addListener: (fn: RuntimeListener) => { listeners.push(fn); },47 removeListener: (fn: RuntimeListener) => { listeners = listeners.filter((l) => l !== fn); },48 },49 },50 };51});5253describe("DesktopPairingFlow", () => {54 const cleanups: Array<() => void> = [];55 afterEach(() => {56 while (cleanups.length) cleanups.pop()!();57 });5859 function mount(handlers: { onCancel?: () => void; onDone?: (d?: PairedDevice) => void } = {}) {60 const r = renderView(61 <DesktopPairingFlow62 onCancel={handlers.onCancel ?? (() => {})}63 onDone={handlers.onDone ?? (() => {})}64 />,65 );66 cleanups.push(r.dispose);67 return r;68 }6970 function codeInput(container: HTMLElement): HTMLInputElement {71 return container.querySelector("#desktop-pair-code") as HTMLInputElement;72 }73 function button(container: HTMLElement, label: string): HTMLButtonElement | undefined {74 return Array.from(container.querySelectorAll("button")).find((b) => b.textContent === label) as75 | HTMLButtonElement76 | undefined;77 }7879 it("renders a 6-digit numeric code input", () => {80 const { container } = mount();81 const input = codeInput(container);82 expect(input).toBeTruthy();83 expect(input.getAttribute("inputmode")).toBe("numeric");84 expect(input.getAttribute("maxlength")).toBe("6");85 86 inputText(input, "12ab3456789");87 expect(input.value).toBe("123456");88 });8990 it("Connect sends PAIR_WITH_DESKTOP with the typed code", async () => {91 const { container } = mount();92 inputText(codeInput(container), "428913");93 const connect = button(container, "Connect")!;94 expect(connect.disabled).toBe(false);95 connect.click();96 await Promise.resolve();97 expect(sendMessage).toHaveBeenCalledWith({98 type: "PAIR_WITH_DESKTOP",99 payload: { code: "428913" },100 });101 102 expect(container.textContent).toContain("Searching for helper…");103 });104105 it("Connect stays disabled until 6 digits are entered", () => {106 const { container } = mount();107 inputText(codeInput(container), "123");108 expect(button(container, "Connect")!.disabled).toBe(true);109 inputText(codeInput(container), "123456");110 expect(button(container, "Connect")!.disabled).toBe(false);111 });112113 it("shows the SAS on a DESKTOP_PAIR_SAS push, with Confirm/Cancel", async () => {114 const { container } = mount();115 inputText(codeInput(container), "428913");116 button(container, "Connect")!.click();117 await Promise.resolve();118119 pushFromBackground("DESKTOP_PAIR_SAS", { sas: "895245" });120121 expect(container.textContent).toContain("Confirm this code matches on your computer:");122 expect(container.textContent).toContain("895245");123 expect(button(container, "Confirm")).toBeTruthy();124 expect(button(container, "Cancel")).toBeTruthy();125 });126127 it("Confirm sends DESKTOP_PAIR_CONFIRM { confirmed: true }", async () => {128 const { container } = mount();129 inputText(codeInput(container), "428913");130 button(container, "Connect")!.click();131 await Promise.resolve();132 pushFromBackground("DESKTOP_PAIR_SAS", { sas: "895245" });133134 button(container, "Confirm")!.click();135 await Promise.resolve();136 expect(sendMessage).toHaveBeenCalledWith({137 type: "DESKTOP_PAIR_CONFIRM",138 payload: { confirmed: true },139 });140 });141142 it("Cancel on the SAS gate sends DESKTOP_PAIR_CONFIRM { confirmed: false } and shows mismatch copy", async () => {143 const { container } = mount();144 inputText(codeInput(container), "428913");145 button(container, "Connect")!.click();146 await Promise.resolve();147 pushFromBackground("DESKTOP_PAIR_SAS", { sas: "895245" });148149 button(container, "Cancel")!.click();150 await Promise.resolve();151 expect(sendMessage).toHaveBeenCalledWith({152 type: "DESKTOP_PAIR_CONFIRM",153 payload: { confirmed: false },154 });155 expect(container.textContent).toContain("Codes don't match");156 });157158 it("shows the combined 'not found or expired' copy on invalid_code (socket close)", async () => {159 const { container } = mount();160 inputText(codeInput(container), "000000");161 button(container, "Connect")!.click();162 await Promise.resolve();163164 165 166 pushFromBackground("DESKTOP_PAIR_RESULT", { status: "error", error: "invalid_code" });167168 expect(container.textContent).toContain("Code not found or expired");169 expect(button(container, "Try again")).toBeTruthy();170 });171172 it("shows the 'no computer found' copy on no_host_found", async () => {173 const { container } = mount();174 inputText(codeInput(container), "111111");175 button(container, "Connect")!.click();176 await Promise.resolve();177 pushFromBackground("DESKTOP_PAIR_RESULT", { status: "error", error: "no_host_found" });178 expect(container.textContent).toContain("Couldn't find your computer");179 });180181 it("renders Paired! and the device name on a complete result", async () => {182 const onDone = vi.fn();183 const device: PairedDevice = { deviceId: "d1", deviceName: "June's iMac", deviceType: "desktop", pairedAt: Date.now() };184 const { container } = mount({ onDone });185 inputText(codeInput(container), "428913");186 button(container, "Connect")!.click();187 await Promise.resolve();188 pushFromBackground("DESKTOP_PAIR_SAS", { sas: "895245" });189 button(container, "Confirm")!.click();190 await Promise.resolve();191192 pushFromBackground("DESKTOP_PAIR_RESULT", { status: "complete", device });193 expect(container.textContent).toContain("Paired!");194 expect(container.textContent).toContain("Connected to June's iMac");195 });196197 it("auto-dismisses after the paired state settles", async () => {198 vi.useFakeTimers();199 try {200 const onDone = vi.fn();201 const device: PairedDevice = { deviceId: "d1", deviceName: "Mac", deviceType: "desktop", pairedAt: 0 };202 const { container } = mount({ onDone });203 inputText(codeInput(container), "428913");204 button(container, "Connect")!.click();205 await Promise.resolve();206 pushFromBackground("DESKTOP_PAIR_SAS", { sas: "895245" });207 button(container, "Confirm")!.click();208 await Promise.resolve();209 pushFromBackground("DESKTOP_PAIR_RESULT", { status: "complete", device });210 vi.advanceTimersByTime(2000);211 expect(onDone).toHaveBeenCalled();212 } finally {213 vi.useRealTimers();214 }215 });216217 it("surfaces a synchronous start failure as an error phase", async () => {218 sendMessage.mockResolvedValueOnce({ ok: false, error: "vault_locked" });219 const { container } = mount();220 inputText(codeInput(container), "428913");221 button(container, "Connect")!.click();222 await Promise.resolve();223 await Promise.resolve();224 expect(container.textContent).toContain("Unlock Peach first");225 });226});