123456789101112131415import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";161718const { mockSendMessage } = vi.hoisted(() => ({ mockSendMessage: vi.fn() }));19vi.mock("../../shared/bridge", () => ({ sendMessage: mockSendMessage }));202122let startResponse: { ok: boolean; error?: string } = { ok: true };2324import { ChirpSyncView } from "./ChirpSync";25import type { PairedDevice } from "../../shared/sync-types";26import { renderView, inputText } from "./view-test-utils";272829type RuntimeListener = (msg: { type?: string; payload?: unknown }) => void;30let listeners: RuntimeListener[] = [];3132function pushFromBackground(type: string, payload: unknown): void {33 for (const l of [...listeners]) l({ type, payload });34}3536beforeEach(() => {37 listeners = [];38 startResponse = { ok: true };39 mockSendMessage.mockReset();40 mockSendMessage.mockImplementation(async (msg: { type?: string }) => {41 if (msg?.type === "PAIR_WITH_DESKTOP") return startResponse;42 return { ok: true };43 });44 (globalThis as unknown as { chrome: unknown }).chrome = {45 runtime: {46 onMessage: {47 addListener: (fn: RuntimeListener) => { listeners.push(fn); },48 removeListener: (fn: RuntimeListener) => { listeners = listeners.filter((l) => l !== fn); },49 },50 },51 };52});5354describe("ChirpSync — desktop helper pairing", () => {55 const cleanups: Array<() => void> = [];56 afterEach(() => {57 while (cleanups.length) cleanups.pop()!();58 });5960 61 62 function mount(onBack: () => void = () => {}) {63 const r = renderView(64 <ChirpSyncView isPro={true} initialPair="desktop" onBack={onBack} onLearnPro={() => {}} />,65 );66 cleanups.push(r.dispose);67 return r;68 }6970 function buttonByText(container: HTMLElement, text: string): HTMLButtonElement | undefined {71 return Array.from(container.querySelectorAll("button")).find(72 (b) => (b.textContent ?? "").trim() === text,73 ) as HTMLButtonElement | undefined;74 }75 function codeInput(container: HTMLElement): HTMLInputElement {76 return container.querySelector("#desktop-pair-code") as HTMLInputElement;77 }7879 80 function gotoDesktopEnter(_container: HTMLElement): void {81 82 }8384 it("opens straight into the desktop code flow (no method-select screen)", () => {85 const { container } = mount();86 expect(container.textContent).not.toContain("How do you want to pair?");87 88 expect(codeInput(container)).toBeTruthy();89 expect(buttonByText(container, "Connect")).toBeTruthy();90 });9192 it("renders a 6-digit numeric code input with birds present", () => {93 const { container } = mount();94 gotoDesktopEnter(container);95 const input = codeInput(container);96 expect(input).toBeTruthy();97 expect(input.getAttribute("inputmode")).toBe("numeric");98 expect(input.getAttribute("maxlength")).toBe("6");99 100 expect(container.querySelector(".peach-chirp-bird-desktop")).toBeTruthy();101 102 inputText(input, "12ab3456789");103 expect(input.value).toBe("123456");104 });105106 it("Connect sends PAIR_WITH_DESKTOP with the typed code, then searches", async () => {107 const { container } = mount();108 gotoDesktopEnter(container);109 inputText(codeInput(container), "428913");110 const connect = buttonByText(container, "Connect")!;111 expect(connect.disabled).toBe(false);112 connect.click();113 await Promise.resolve();114 expect(mockSendMessage).toHaveBeenCalledWith({115 type: "PAIR_WITH_DESKTOP",116 payload: { code: "428913" },117 });118 expect(container.textContent).toContain("Searching for helper…");119 });120121 it("Connect stays disabled until 6 digits are entered", () => {122 const { container } = mount();123 gotoDesktopEnter(container);124 inputText(codeInput(container), "123");125 expect(buttonByText(container, "Connect")!.disabled).toBe(true);126 inputText(codeInput(container), "123456");127 expect(buttonByText(container, "Connect")!.disabled).toBe(false);128 });129130 it("shows the SAS on a DESKTOP_PAIR_SAS push, with Confirm/Cancel", async () => {131 const { container } = mount();132 gotoDesktopEnter(container);133 inputText(codeInput(container), "428913");134 buttonByText(container, "Connect")!.click();135 await Promise.resolve();136137 pushFromBackground("DESKTOP_PAIR_SAS", { sas: "895245" });138139 expect(container.textContent).toContain("Confirm this code matches on your computer:");140 expect(container.textContent).toContain("895245");141 expect(buttonByText(container, "Confirm")).toBeTruthy();142 expect(buttonByText(container, "Cancel")).toBeTruthy();143 });144145 it("Confirm sends DESKTOP_PAIR_CONFIRM { confirmed: true }", async () => {146 const { container } = mount();147 gotoDesktopEnter(container);148 inputText(codeInput(container), "428913");149 buttonByText(container, "Connect")!.click();150 await Promise.resolve();151 pushFromBackground("DESKTOP_PAIR_SAS", { sas: "895245" });152153 buttonByText(container, "Confirm")!.click();154 await Promise.resolve();155 expect(mockSendMessage).toHaveBeenCalledWith({156 type: "DESKTOP_PAIR_CONFIRM",157 payload: { confirmed: true },158 });159 });160161 it("Cancel on the SAS gate sends { confirmed: false } and shows mismatch copy", async () => {162 const { container } = mount();163 gotoDesktopEnter(container);164 inputText(codeInput(container), "428913");165 buttonByText(container, "Connect")!.click();166 await Promise.resolve();167 pushFromBackground("DESKTOP_PAIR_SAS", { sas: "895245" });168169 buttonByText(container, "Cancel")!.click();170 await Promise.resolve();171 expect(mockSendMessage).toHaveBeenCalledWith({172 type: "DESKTOP_PAIR_CONFIRM",173 payload: { confirmed: false },174 });175 expect(container.textContent).toContain("Codes don't match");176 });177178 it("shows the combined 'not found or expired' copy on invalid_code", async () => {179 const { container } = mount();180 gotoDesktopEnter(container);181 inputText(codeInput(container), "000000");182 buttonByText(container, "Connect")!.click();183 await Promise.resolve();184185 186 187 pushFromBackground("DESKTOP_PAIR_RESULT", { status: "error", error: "invalid_code" });188189 expect(container.textContent).toContain("Code not found or expired");190 expect(buttonByText(container, "Try again")).toBeTruthy();191 });192193 it("shows the 'no computer found' copy on no_host_found", async () => {194 const { container } = mount();195 gotoDesktopEnter(container);196 inputText(codeInput(container), "111111");197 buttonByText(container, "Connect")!.click();198 await Promise.resolve();199 pushFromBackground("DESKTOP_PAIR_RESULT", { status: "error", error: "no_host_found" });200 expect(container.textContent).toContain("Couldn't find your computer");201 });202203 it("renders Paired! and the device name on a complete result", async () => {204 const device: PairedDevice = { deviceId: "d1", deviceName: "June's iMac", deviceType: "desktop", pairedAt: 1700000000000 };205 const { container } = mount();206 gotoDesktopEnter(container);207 inputText(codeInput(container), "428913");208 buttonByText(container, "Connect")!.click();209 await Promise.resolve();210 pushFromBackground("DESKTOP_PAIR_SAS", { sas: "895245" });211 buttonByText(container, "Confirm")!.click();212 await Promise.resolve();213214 pushFromBackground("DESKTOP_PAIR_RESULT", { status: "complete", device });215 expect(container.textContent).toContain("Paired!");216 expect(container.textContent).toContain("Connected to June's iMac");217 });218219 it("auto-dismisses back to settings after the paired state settles", async () => {220 vi.useFakeTimers();221 try {222 const device: PairedDevice = { deviceId: "d1", deviceName: "Mac", deviceType: "desktop", pairedAt: 0 };223 const onBack = vi.fn();224 const { container } = mount(onBack);225 gotoDesktopEnter(container);226 inputText(codeInput(container), "428913");227 buttonByText(container, "Connect")!.click();228 await Promise.resolve();229 pushFromBackground("DESKTOP_PAIR_SAS", { sas: "895245" });230 buttonByText(container, "Confirm")!.click();231 await Promise.resolve();232 pushFromBackground("DESKTOP_PAIR_RESULT", { status: "complete", device });233 expect(container.textContent).toContain("Paired!");234 vi.advanceTimersByTime(2000);235 236 expect(onBack).toHaveBeenCalled();237 } finally {238 vi.useRealTimers();239 }240 });241242 it("surfaces a synchronous start failure (vault_locked) as an error", async () => {243 startResponse = { ok: false, error: "vault_locked" };244 const { container } = mount();245 gotoDesktopEnter(container);246 inputText(codeInput(container), "428913");247 buttonByText(container, "Connect")!.click();248 await Promise.resolve();249 await Promise.resolve();250 expect(container.textContent).toContain("Unlock Peach first");251 });252});