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 gotoDesktopEnter(container: HTMLElement): void {83 84 buttonContaining(container, "Enter a code")!.click();85 }8687 it("offers chirp and code as direct options on the rest screen", () => {88 const { container } = mount();89 90 expect(container.textContent).not.toContain("How do you want to pair?");9192 const sameRoom = buttonContaining(container, "Same room")!;93 const code = buttonContaining(container, "Enter a code")!;94 expect(sameRoom).toBeTruthy();95 expect(code).toBeTruthy();96 97 expect(code.textContent).toContain("Type the code from your desktop sync helper");98 expect(sameRoom.querySelector(".peach-pair-method-icon")).toBeTruthy();99 expect(code.querySelector(".peach-pair-method-icon")).toBeTruthy();100 });101102 it("renders a 6-digit numeric code input with birds present", () => {103 const { container } = mount();104 gotoDesktopEnter(container);105 const input = codeInput(container);106 expect(input).toBeTruthy();107 expect(input.getAttribute("inputmode")).toBe("numeric");108 expect(input.getAttribute("maxlength")).toBe("6");109 110 expect(container.querySelector(".peach-chirp-bird-desktop")).toBeTruthy();111 112 inputText(input, "12ab3456789");113 expect(input.value).toBe("123456");114 });115116 it("Connect sends PAIR_WITH_DESKTOP with the typed code, then searches", async () => {117 const { container } = mount();118 gotoDesktopEnter(container);119 inputText(codeInput(container), "428913");120 const connect = buttonByText(container, "Connect")!;121 expect(connect.disabled).toBe(false);122 connect.click();123 await Promise.resolve();124 expect(mockSendMessage).toHaveBeenCalledWith({125 type: "PAIR_WITH_DESKTOP",126 payload: { code: "428913" },127 });128 expect(container.textContent).toContain("Searching for helper…");129 });130131 it("Connect stays disabled until 6 digits are entered", () => {132 const { container } = mount();133 gotoDesktopEnter(container);134 inputText(codeInput(container), "123");135 expect(buttonByText(container, "Connect")!.disabled).toBe(true);136 inputText(codeInput(container), "123456");137 expect(buttonByText(container, "Connect")!.disabled).toBe(false);138 });139140 it("shows the SAS on a DESKTOP_PAIR_SAS push, with Confirm/Cancel", async () => {141 const { container } = mount();142 gotoDesktopEnter(container);143 inputText(codeInput(container), "428913");144 buttonByText(container, "Connect")!.click();145 await Promise.resolve();146147 pushFromBackground("DESKTOP_PAIR_SAS", { sas: "895245" });148149 expect(container.textContent).toContain("Confirm this code matches on your computer:");150 expect(container.textContent).toContain("895245");151 expect(buttonByText(container, "Confirm")).toBeTruthy();152 expect(buttonByText(container, "Cancel")).toBeTruthy();153 });154155 it("Confirm sends DESKTOP_PAIR_CONFIRM { confirmed: true }", async () => {156 const { container } = mount();157 gotoDesktopEnter(container);158 inputText(codeInput(container), "428913");159 buttonByText(container, "Connect")!.click();160 await Promise.resolve();161 pushFromBackground("DESKTOP_PAIR_SAS", { sas: "895245" });162163 buttonByText(container, "Confirm")!.click();164 await Promise.resolve();165 expect(mockSendMessage).toHaveBeenCalledWith({166 type: "DESKTOP_PAIR_CONFIRM",167 payload: { confirmed: true },168 });169 });170171 it("Cancel on the SAS gate sends { confirmed: false } and shows mismatch copy", async () => {172 const { container } = mount();173 gotoDesktopEnter(container);174 inputText(codeInput(container), "428913");175 buttonByText(container, "Connect")!.click();176 await Promise.resolve();177 pushFromBackground("DESKTOP_PAIR_SAS", { sas: "895245" });178179 buttonByText(container, "Cancel")!.click();180 await Promise.resolve();181 expect(mockSendMessage).toHaveBeenCalledWith({182 type: "DESKTOP_PAIR_CONFIRM",183 payload: { confirmed: false },184 });185 expect(container.textContent).toContain("Codes don't match");186 });187188 it("shows the combined 'not found or expired' copy on invalid_code", async () => {189 const { container } = mount();190 gotoDesktopEnter(container);191 inputText(codeInput(container), "000000");192 buttonByText(container, "Connect")!.click();193 await Promise.resolve();194195 196 197 pushFromBackground("DESKTOP_PAIR_RESULT", { status: "error", error: "invalid_code" });198199 expect(container.textContent).toContain("Code not found or expired");200 expect(buttonByText(container, "Try again")).toBeTruthy();201 });202203 it("shows the 'no computer found' copy on no_host_found", async () => {204 const { container } = mount();205 gotoDesktopEnter(container);206 inputText(codeInput(container), "111111");207 buttonByText(container, "Connect")!.click();208 await Promise.resolve();209 pushFromBackground("DESKTOP_PAIR_RESULT", { status: "error", error: "no_host_found" });210 expect(container.textContent).toContain("Couldn't find your computer");211 });212213 it("renders Paired! and the device name on a complete result", async () => {214 const device: PairedDevice = { deviceId: "d1", deviceName: "June's iMac", deviceType: "desktop", pairedAt: 1700000000000 };215 const { container } = mount();216 gotoDesktopEnter(container);217 inputText(codeInput(container), "428913");218 buttonByText(container, "Connect")!.click();219 await Promise.resolve();220 pushFromBackground("DESKTOP_PAIR_SAS", { sas: "895245" });221 buttonByText(container, "Confirm")!.click();222 await Promise.resolve();223224 pushFromBackground("DESKTOP_PAIR_RESULT", { status: "complete", device });225 expect(container.textContent).toContain("Paired!");226 expect(container.textContent).toContain("Connected to June's iMac");227 });228229 it("auto-dismisses to the device list after the paired state settles", async () => {230 vi.useFakeTimers();231 try {232 const device: PairedDevice = { deviceId: "d1", deviceName: "Mac", deviceType: "desktop", pairedAt: 0 };233 const { container } = mount();234 gotoDesktopEnter(container);235 inputText(codeInput(container), "428913");236 buttonByText(container, "Connect")!.click();237 await Promise.resolve();238 pushFromBackground("DESKTOP_PAIR_SAS", { sas: "895245" });239 buttonByText(container, "Confirm")!.click();240 await Promise.resolve();241 pushFromBackground("DESKTOP_PAIR_RESULT", { status: "complete", device });242 expect(container.textContent).toContain("Paired!");243 vi.advanceTimersByTime(2000);244 245 expect(container.textContent).not.toContain("Paired!");246 expect(buttonContaining(container, "Same room")).toBeTruthy();247 expect(buttonContaining(container, "Enter a code")).toBeTruthy();248 } finally {249 vi.useRealTimers();250 }251 });252253 it("surfaces a synchronous start failure (vault_locked) as an error", async () => {254 startResponse = { ok: false, error: "vault_locked" };255 const { container } = mount();256 gotoDesktopEnter(container);257 inputText(codeInput(container), "428913");258 buttonByText(container, "Connect")!.click();259 await Promise.resolve();260 await Promise.resolve();261 expect(container.textContent).toContain("Unlock Peach first");262 });263});