12345678910111213141516import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";171819const { mockSendMessage } = vi.hoisted(() => ({ mockSendMessage: vi.fn() }));20vi.mock("../../shared/bridge", () => ({ sendMessage: mockSendMessage }));212223let startResponse: { ok: boolean; error?: string } = { ok: true };2425import { PairDeviceView } from "./PairDevice";26import type { PairedDevice } from "../../shared/sync-types";27import { renderView, inputText } from "./view-test-utils";282930type RuntimeListener = (msg: { type?: string; payload?: unknown }) => void;31let listeners: RuntimeListener[] = [];3233function pushFromBackground(type: string, payload: unknown): void {34 for (const l of [...listeners]) l({ type, payload });35}3637beforeEach(() => {38 listeners = [];39 startResponse = { ok: true };40 mockSendMessage.mockReset();41 mockSendMessage.mockImplementation(async (msg: { type?: string }) => {42 if (msg?.type === "PAIR_WITH_CODE") return startResponse;43 return { ok: true };44 });45 (globalThis as unknown as { chrome: unknown }).chrome = {46 runtime: {47 onMessage: {48 addListener: (fn: RuntimeListener) => { listeners.push(fn); },49 removeListener: (fn: RuntimeListener) => { listeners = listeners.filter((l) => l !== fn); },50 },51 },52 };53});5455describe("ChirpSync — code pairing", () => {56 const cleanups: Array<() => void> = [];57 afterEach(() => {58 while (cleanups.length) cleanups.pop()!();59 });6061 62 63 function mount(onBack: () => void = () => {}) {64 const r = renderView(65 <PairDeviceView isPro={true} initialPair="code" onBack={onBack} onLearnPro={() => {}} />,66 );67 cleanups.push(r.dispose);68 return r;69 }7071 function buttonByText(container: HTMLElement, text: string): HTMLButtonElement | undefined {72 return Array.from(container.querySelectorAll("button")).find(73 (b) => (b.textContent ?? "").trim() === text,74 ) as HTMLButtonElement | undefined;75 }76 function codeInput(container: HTMLElement): HTMLInputElement {77 return container.querySelector("#pair-code") as HTMLInputElement;78 }7980 81 function gotoDesktopEnter(_container: HTMLElement): void {82 83 }8485 it("opens straight into the desktop code flow (no method-select screen)", () => {86 const { container } = mount();87 expect(container.textContent).not.toContain("How do you want to pair?");88 89 expect(codeInput(container)).toBeTruthy();90 expect(buttonByText(container, "Connect")).toBeTruthy();91 });9293 it("renders a code input sized for a full pairing code, with birds present", () => {94 const { container } = mount();95 gotoDesktopEnter(container);96 const input = codeInput(container);97 expect(input).toBeTruthy();98 99 expect(input.getAttribute("inputmode")).toBeNull();100 expect(input.getAttribute("maxlength")).toBe("32");101 102 expect(container.querySelector(".peach-chirp-bird-desktop")).toBeTruthy();103 });104105 it("uppercases and regroups a pairing code as it is typed", () => {106 const { container } = mount();107 gotoDesktopEnter(container);108 const input = codeInput(container);109 inputText(input, "a74h48j3ki32oep4");110 expect(input.value).toBe("A74H-48J3-KI32-OEP4");111 expect(buttonByText(container, "Connect")!.disabled).toBe(false);112 });113114 it("leaves a short legacy code ungrouped and strips unusable characters", () => {115 const { container } = mount();116 gotoDesktopEnter(container);117 const input = codeInput(container);118 inputText(input, "12_34/56");119 expect(input.value).toBe("123456");120 });121122 it("keeps Connect disabled for input that is neither code form", () => {123 const { container } = mount();124 gotoDesktopEnter(container);125 126 inputText(codeInput(container), "A74H48J3");127 expect(buttonByText(container, "Connect")!.disabled).toBe(true);128 });129130 it("Connect sends PAIR_WITH_CODE with the typed code, then searches", async () => {131 const { container } = mount();132 gotoDesktopEnter(container);133 inputText(codeInput(container), "428913");134 const connect = buttonByText(container, "Connect")!;135 expect(connect.disabled).toBe(false);136 connect.click();137 await Promise.resolve();138 expect(mockSendMessage).toHaveBeenCalledWith({139 type: "PAIR_WITH_CODE",140 payload: { code: "428913" },141 });142 expect(container.textContent).toContain("Connecting…");143 });144145 it("Connect stays disabled until 6 digits are entered", () => {146 const { container } = mount();147 gotoDesktopEnter(container);148 inputText(codeInput(container), "123");149 expect(buttonByText(container, "Connect")!.disabled).toBe(true);150 inputText(codeInput(container), "123456");151 expect(buttonByText(container, "Connect")!.disabled).toBe(false);152 });153154 it("shows the SAS on a CODE_PAIR_SAS push, with Confirm/Cancel", async () => {155 const { container } = mount();156 gotoDesktopEnter(container);157 inputText(codeInput(container), "428913");158 buttonByText(container, "Connect")!.click();159 await Promise.resolve();160161 pushFromBackground("CODE_PAIR_SAS", { sas: "895245" });162163 expect(container.textContent).toContain("Confirm this code matches on your phone:");164 expect(container.textContent).toContain("895245");165 expect(buttonByText(container, "Confirm")).toBeTruthy();166 expect(buttonByText(container, "Cancel")).toBeTruthy();167 });168169 it("Confirm sends CODE_PAIR_CONFIRM { confirmed: true }", async () => {170 const { container } = mount();171 gotoDesktopEnter(container);172 inputText(codeInput(container), "428913");173 buttonByText(container, "Connect")!.click();174 await Promise.resolve();175 pushFromBackground("CODE_PAIR_SAS", { sas: "895245" });176177 buttonByText(container, "Confirm")!.click();178 await Promise.resolve();179 expect(mockSendMessage).toHaveBeenCalledWith({180 type: "CODE_PAIR_CONFIRM",181 payload: { confirmed: true },182 });183 });184185 it("Cancel on the SAS gate sends { confirmed: false } and shows mismatch copy", async () => {186 const { container } = mount();187 gotoDesktopEnter(container);188 inputText(codeInput(container), "428913");189 buttonByText(container, "Connect")!.click();190 await Promise.resolve();191 pushFromBackground("CODE_PAIR_SAS", { sas: "895245" });192193 buttonByText(container, "Cancel")!.click();194 await Promise.resolve();195 expect(mockSendMessage).toHaveBeenCalledWith({196 type: "CODE_PAIR_CONFIRM",197 payload: { confirmed: false },198 });199 expect(container.textContent).toContain("Codes don't match");200 });201202 it("shows the combined 'not found or expired' copy on invalid_code", async () => {203 const { container } = mount();204 gotoDesktopEnter(container);205 inputText(codeInput(container), "000000");206 buttonByText(container, "Connect")!.click();207 await Promise.resolve();208209 210 211 pushFromBackground("CODE_PAIR_RESULT", { status: "error", error: "invalid_code" });212213 expect(container.textContent).toContain("Code not found or expired");214 expect(buttonByText(container, "Try again")).toBeTruthy();215 });216217 it("shows the 'couldn't reach your phone' copy on no_host_found", async () => {218 const { container } = mount();219 gotoDesktopEnter(container);220 inputText(codeInput(container), "111111");221 buttonByText(container, "Connect")!.click();222 await Promise.resolve();223 pushFromBackground("CODE_PAIR_RESULT", { status: "error", error: "no_host_found" });224 expect(container.textContent).toContain("Couldn't reach your phone");225 });226227 it("renders Paired! and the device name on a complete result", async () => {228 const device: PairedDevice = { deviceId: "d1", deviceName: "June's iMac", deviceType: "desktop", pairedAt: 1700000000000 };229 const { container } = mount();230 gotoDesktopEnter(container);231 inputText(codeInput(container), "428913");232 buttonByText(container, "Connect")!.click();233 await Promise.resolve();234 pushFromBackground("CODE_PAIR_SAS", { sas: "895245" });235 buttonByText(container, "Confirm")!.click();236 await Promise.resolve();237238 pushFromBackground("CODE_PAIR_RESULT", { status: "complete", device });239 expect(container.textContent).toContain("Paired!");240 expect(container.textContent).toContain("Connected to June's iMac");241 });242243 it("auto-dismisses back to settings after the paired state settles", async () => {244 vi.useFakeTimers();245 try {246 const device: PairedDevice = { deviceId: "d1", deviceName: "Mac", deviceType: "desktop", pairedAt: 0 };247 const onBack = vi.fn();248 const { container } = mount(onBack);249 gotoDesktopEnter(container);250 inputText(codeInput(container), "428913");251 buttonByText(container, "Connect")!.click();252 await Promise.resolve();253 pushFromBackground("CODE_PAIR_SAS", { sas: "895245" });254 buttonByText(container, "Confirm")!.click();255 await Promise.resolve();256 pushFromBackground("CODE_PAIR_RESULT", { status: "complete", device });257 expect(container.textContent).toContain("Paired!");258 vi.advanceTimersByTime(2000);259 260 expect(onBack).toHaveBeenCalled();261 } finally {262 vi.useRealTimers();263 }264 });265266 it("surfaces a synchronous start failure (vault_locked) as an error", async () => {267 startResponse = { ok: false, error: "vault_locked" };268 const { container } = mount();269 gotoDesktopEnter(container);270 inputText(codeInput(container), "428913");271 buttonByText(container, "Connect")!.click();272 await Promise.resolve();273 await Promise.resolve();274 expect(container.textContent).toContain("Unlock Peach first");275 });276});