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 function mount() {61 const r = renderView(62 <ChirpSyncView isPro={true} onBack={() => {}} onLearnPro={() => {}} />,63 );64 cleanups.push(r.dispose);65 return r;66 }6768 function buttonByText(container: HTMLElement, text: string): HTMLButtonElement | undefined {69 return Array.from(container.querySelectorAll("button")).find(70 (b) => (b.textContent ?? "").trim() === text,71 ) as HTMLButtonElement | undefined;72 }73 function buttonContaining(container: HTMLElement, text: string): HTMLButtonElement | undefined {74 return Array.from(container.querySelectorAll("button")).find(75 (b) => (b.textContent ?? "").includes(text),76 ) as HTMLButtonElement | undefined;77 }78 function codeInput(container: HTMLElement): HTMLInputElement {79 return container.querySelector("#desktop-pair-code") as HTMLInputElement;80 }8182 function gotoMethodSelect(container: HTMLElement): void {83 buttonByText(container, "Pair a device")!.click();84 }85 function gotoDesktopEnter(container: HTMLElement): void {86 gotoMethodSelect(container);87 buttonContaining(container, "Desktop helper")!.click();88 }8990 it("offers both pairing methods as icon cards on the method-select screen", () => {91 const { container } = mount();92 gotoMethodSelect(container);93 expect(container.textContent).toContain("How do you want to pair?");94 95 expect(container.textContent).not.toContain("In the same room? Use a chirp");9697 const sameRoom = buttonContaining(container, "Same room")!;98 const desktop = buttonContaining(container, "Desktop helper")!;99 expect(sameRoom).toBeTruthy();100 expect(desktop).toBeTruthy();101 102 expect(sameRoom.textContent).toContain("Chirp — plays an inaudible sound");103 expect(desktop.textContent).toContain("Enter the code shown by peach-sync");104 expect(sameRoom.querySelector(".peach-pair-method-icon")).toBeTruthy();105 expect(desktop.querySelector(".peach-pair-method-icon")).toBeTruthy();106 });107108 it("renders a 6-digit numeric code input with birds present", () => {109 const { container } = mount();110 gotoDesktopEnter(container);111 const input = codeInput(container);112 expect(input).toBeTruthy();113 expect(input.getAttribute("inputmode")).toBe("numeric");114 expect(input.getAttribute("maxlength")).toBe("6");115 116 expect(container.querySelector(".peach-chirp-bird-desktop")).toBeTruthy();117 118 inputText(input, "12ab3456789");119 expect(input.value).toBe("123456");120 });121122 it("Connect sends PAIR_WITH_DESKTOP with the typed code, then searches", async () => {123 const { container } = mount();124 gotoDesktopEnter(container);125 inputText(codeInput(container), "428913");126 const connect = buttonByText(container, "Connect")!;127 expect(connect.disabled).toBe(false);128 connect.click();129 await Promise.resolve();130 expect(mockSendMessage).toHaveBeenCalledWith({131 type: "PAIR_WITH_DESKTOP",132 payload: { code: "428913" },133 });134 expect(container.textContent).toContain("Searching for helper…");135 });136137 it("Connect stays disabled until 6 digits are entered", () => {138 const { container } = mount();139 gotoDesktopEnter(container);140 inputText(codeInput(container), "123");141 expect(buttonByText(container, "Connect")!.disabled).toBe(true);142 inputText(codeInput(container), "123456");143 expect(buttonByText(container, "Connect")!.disabled).toBe(false);144 });145146 it("shows the SAS on a DESKTOP_PAIR_SAS push, with Confirm/Cancel", async () => {147 const { container } = mount();148 gotoDesktopEnter(container);149 inputText(codeInput(container), "428913");150 buttonByText(container, "Connect")!.click();151 await Promise.resolve();152153 pushFromBackground("DESKTOP_PAIR_SAS", { sas: "895245" });154155 expect(container.textContent).toContain("Confirm this code matches on your computer:");156 expect(container.textContent).toContain("895245");157 expect(buttonByText(container, "Confirm")).toBeTruthy();158 expect(buttonByText(container, "Cancel")).toBeTruthy();159 });160161 it("Confirm sends DESKTOP_PAIR_CONFIRM { confirmed: true }", 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, "Confirm")!.click();170 await Promise.resolve();171 expect(mockSendMessage).toHaveBeenCalledWith({172 type: "DESKTOP_PAIR_CONFIRM",173 payload: { confirmed: true },174 });175 });176177 it("Cancel on the SAS gate sends { confirmed: false } and shows mismatch copy", async () => {178 const { container } = mount();179 gotoDesktopEnter(container);180 inputText(codeInput(container), "428913");181 buttonByText(container, "Connect")!.click();182 await Promise.resolve();183 pushFromBackground("DESKTOP_PAIR_SAS", { sas: "895245" });184185 buttonByText(container, "Cancel")!.click();186 await Promise.resolve();187 expect(mockSendMessage).toHaveBeenCalledWith({188 type: "DESKTOP_PAIR_CONFIRM",189 payload: { confirmed: false },190 });191 expect(container.textContent).toContain("Codes don't match");192 });193194 it("shows the combined 'not found or expired' copy on invalid_code", async () => {195 const { container } = mount();196 gotoDesktopEnter(container);197 inputText(codeInput(container), "000000");198 buttonByText(container, "Connect")!.click();199 await Promise.resolve();200201 202 203 pushFromBackground("DESKTOP_PAIR_RESULT", { status: "error", error: "invalid_code" });204205 expect(container.textContent).toContain("Code not found or expired");206 expect(buttonByText(container, "Try again")).toBeTruthy();207 });208209 it("shows the 'no computer found' copy on no_host_found", async () => {210 const { container } = mount();211 gotoDesktopEnter(container);212 inputText(codeInput(container), "111111");213 buttonByText(container, "Connect")!.click();214 await Promise.resolve();215 pushFromBackground("DESKTOP_PAIR_RESULT", { status: "error", error: "no_host_found" });216 expect(container.textContent).toContain("Couldn't find your computer");217 });218219 it("renders Paired! and the device name on a complete result", async () => {220 const device: PairedDevice = { deviceId: "d1", deviceName: "June's iMac", deviceType: "desktop", pairedAt: 1700000000000 };221 const { container } = mount();222 gotoDesktopEnter(container);223 inputText(codeInput(container), "428913");224 buttonByText(container, "Connect")!.click();225 await Promise.resolve();226 pushFromBackground("DESKTOP_PAIR_SAS", { sas: "895245" });227 buttonByText(container, "Confirm")!.click();228 await Promise.resolve();229230 pushFromBackground("DESKTOP_PAIR_RESULT", { status: "complete", device });231 expect(container.textContent).toContain("Paired!");232 expect(container.textContent).toContain("Connected to June's iMac");233 });234235 it("auto-dismisses to the device list after the paired state settles", async () => {236 vi.useFakeTimers();237 try {238 const device: PairedDevice = { deviceId: "d1", deviceName: "Mac", deviceType: "desktop", pairedAt: 0 };239 const { container } = mount();240 gotoDesktopEnter(container);241 inputText(codeInput(container), "428913");242 buttonByText(container, "Connect")!.click();243 await Promise.resolve();244 pushFromBackground("DESKTOP_PAIR_SAS", { sas: "895245" });245 buttonByText(container, "Confirm")!.click();246 await Promise.resolve();247 pushFromBackground("DESKTOP_PAIR_RESULT", { status: "complete", device });248 expect(container.textContent).toContain("Paired!");249 vi.advanceTimersByTime(2000);250 251 expect(container.textContent).not.toContain("Paired!");252 expect(buttonByText(container, "Pair a device")).toBeTruthy();253 } finally {254 vi.useRealTimers();255 }256 });257258 it("surfaces a synchronous start failure (vault_locked) as an error", async () => {259 startResponse = { ok: false, error: "vault_locked" };260 const { container } = mount();261 gotoDesktopEnter(container);262 inputText(codeInput(container), "428913");263 buttonByText(container, "Connect")!.click();264 await Promise.resolve();265 await Promise.resolve();266 expect(container.textContent).toContain("Unlock Peach first");267 });268});