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 on the method-select screen", () => {91 const { container } = mount();92 gotoMethodSelect(container);93 expect(container.textContent).toContain("How do you want to pair?");94 expect(buttonContaining(container, "Same room")).toBeTruthy();95 expect(buttonContaining(container, "Desktop helper")).toBeTruthy();96 });9798 it("renders a 6-digit numeric code input with birds present", () => {99 const { container } = mount();100 gotoDesktopEnter(container);101 const input = codeInput(container);102 expect(input).toBeTruthy();103 expect(input.getAttribute("inputmode")).toBe("numeric");104 expect(input.getAttribute("maxlength")).toBe("6");105 106 expect(container.querySelector(".peach-chirp-bird-desktop")).toBeTruthy();107 108 inputText(input, "12ab3456789");109 expect(input.value).toBe("123456");110 });111112 it("Connect sends PAIR_WITH_DESKTOP with the typed code, then searches", async () => {113 const { container } = mount();114 gotoDesktopEnter(container);115 inputText(codeInput(container), "428913");116 const connect = buttonByText(container, "Connect")!;117 expect(connect.disabled).toBe(false);118 connect.click();119 await Promise.resolve();120 expect(mockSendMessage).toHaveBeenCalledWith({121 type: "PAIR_WITH_DESKTOP",122 payload: { code: "428913" },123 });124 expect(container.textContent).toContain("Searching for helper…");125 });126127 it("Connect stays disabled until 6 digits are entered", () => {128 const { container } = mount();129 gotoDesktopEnter(container);130 inputText(codeInput(container), "123");131 expect(buttonByText(container, "Connect")!.disabled).toBe(true);132 inputText(codeInput(container), "123456");133 expect(buttonByText(container, "Connect")!.disabled).toBe(false);134 });135136 it("shows the SAS on a DESKTOP_PAIR_SAS push, with Confirm/Cancel", async () => {137 const { container } = mount();138 gotoDesktopEnter(container);139 inputText(codeInput(container), "428913");140 buttonByText(container, "Connect")!.click();141 await Promise.resolve();142143 pushFromBackground("DESKTOP_PAIR_SAS", { sas: "895245" });144145 expect(container.textContent).toContain("Confirm this code matches on your computer:");146 expect(container.textContent).toContain("895245");147 expect(buttonByText(container, "Confirm")).toBeTruthy();148 expect(buttonByText(container, "Cancel")).toBeTruthy();149 });150151 it("Confirm sends DESKTOP_PAIR_CONFIRM { confirmed: true }", async () => {152 const { container } = mount();153 gotoDesktopEnter(container);154 inputText(codeInput(container), "428913");155 buttonByText(container, "Connect")!.click();156 await Promise.resolve();157 pushFromBackground("DESKTOP_PAIR_SAS", { sas: "895245" });158159 buttonByText(container, "Confirm")!.click();160 await Promise.resolve();161 expect(mockSendMessage).toHaveBeenCalledWith({162 type: "DESKTOP_PAIR_CONFIRM",163 payload: { confirmed: true },164 });165 });166167 it("Cancel on the SAS gate sends { confirmed: false } and shows mismatch copy", async () => {168 const { container } = mount();169 gotoDesktopEnter(container);170 inputText(codeInput(container), "428913");171 buttonByText(container, "Connect")!.click();172 await Promise.resolve();173 pushFromBackground("DESKTOP_PAIR_SAS", { sas: "895245" });174175 buttonByText(container, "Cancel")!.click();176 await Promise.resolve();177 expect(mockSendMessage).toHaveBeenCalledWith({178 type: "DESKTOP_PAIR_CONFIRM",179 payload: { confirmed: false },180 });181 expect(container.textContent).toContain("Codes don't match");182 });183184 it("shows the combined 'not found or expired' copy on invalid_code", async () => {185 const { container } = mount();186 gotoDesktopEnter(container);187 inputText(codeInput(container), "000000");188 buttonByText(container, "Connect")!.click();189 await Promise.resolve();190191 192 193 pushFromBackground("DESKTOP_PAIR_RESULT", { status: "error", error: "invalid_code" });194195 expect(container.textContent).toContain("Code not found or expired");196 expect(buttonByText(container, "Try again")).toBeTruthy();197 });198199 it("shows the 'no computer found' copy on no_host_found", async () => {200 const { container } = mount();201 gotoDesktopEnter(container);202 inputText(codeInput(container), "111111");203 buttonByText(container, "Connect")!.click();204 await Promise.resolve();205 pushFromBackground("DESKTOP_PAIR_RESULT", { status: "error", error: "no_host_found" });206 expect(container.textContent).toContain("Couldn't find your computer");207 });208209 it("renders Paired! and the device name on a complete result", async () => {210 const device: PairedDevice = { deviceId: "d1", deviceName: "June's iMac", deviceType: "desktop", pairedAt: 1700000000000 };211 const { container } = mount();212 gotoDesktopEnter(container);213 inputText(codeInput(container), "428913");214 buttonByText(container, "Connect")!.click();215 await Promise.resolve();216 pushFromBackground("DESKTOP_PAIR_SAS", { sas: "895245" });217 buttonByText(container, "Confirm")!.click();218 await Promise.resolve();219220 pushFromBackground("DESKTOP_PAIR_RESULT", { status: "complete", device });221 expect(container.textContent).toContain("Paired!");222 expect(container.textContent).toContain("Connected to June's iMac");223 });224225 it("auto-dismisses to the device list after the paired state settles", async () => {226 vi.useFakeTimers();227 try {228 const device: PairedDevice = { deviceId: "d1", deviceName: "Mac", deviceType: "desktop", pairedAt: 0 };229 const { container } = mount();230 gotoDesktopEnter(container);231 inputText(codeInput(container), "428913");232 buttonByText(container, "Connect")!.click();233 await Promise.resolve();234 pushFromBackground("DESKTOP_PAIR_SAS", { sas: "895245" });235 buttonByText(container, "Confirm")!.click();236 await Promise.resolve();237 pushFromBackground("DESKTOP_PAIR_RESULT", { status: "complete", device });238 expect(container.textContent).toContain("Paired!");239 vi.advanceTimersByTime(2000);240 241 expect(container.textContent).not.toContain("Paired!");242 expect(buttonByText(container, "Pair a device")).toBeTruthy();243 } finally {244 vi.useRealTimers();245 }246 });247248 it("surfaces a synchronous start failure (vault_locked) as an error", async () => {249 startResponse = { ok: false, error: "vault_locked" };250 const { container } = mount();251 gotoDesktopEnter(container);252 inputText(codeInput(container), "428913");253 buttonByText(container, "Connect")!.click();254 await Promise.resolve();255 await Promise.resolve();256 expect(container.textContent).toContain("Unlock Peach first");257 });258});