// @vitest-environment jsdom import { afterEach, describe, expect, it, vi } from "vitest"; import { ChirpPairingFlow } from "./ChirpPairingFlow"; import type { PairingProgress } from "../../shared/sync-types"; import { renderView } from "../views/view-test-utils"; function progress(phase: PairingProgress["phase"], extra: Partial = {}): PairingProgress { return { phase, ...extra }; } describe("ChirpPairingFlow", () => { const cleanups: Array<() => void> = []; afterEach(() => { while (cleanups.length) cleanups.pop()!(); }); function mount(progress: PairingProgress, handlers: { onCancel?: () => void; onRetry?: () => void; onDismiss?: () => void } = {}) { const r = renderView( {})} onRetry={handlers.onRetry ?? (() => {})} onDismiss={handlers.onDismiss ?? (() => {})} />, ); cleanups.push(r.dispose); return r; } it("renders the listening copy in Act 1", () => { const { container } = mount(progress("listening")); expect(container.textContent).toContain("Hold your phone near your laptop"); expect(container.textContent).toContain("Peach will pair automatically"); }); it("renders the handshake copy in Act 2", () => { const { container } = mount(progress("handshake")); expect(container.textContent).toContain("Pairing…"); }); it("renders the paired copy with the peer device name", () => { const { container } = mount(progress("paired", { peerDeviceName: "June's Pixel 9" })); expect(container.textContent).toContain("Paired!"); expect(container.textContent).toContain("Connected to June's Pixel 9"); }); it("Cancel button calls onCancel in the listening state", () => { const onCancel = vi.fn(); const { container } = mount(progress("listening"), { onCancel }); const btn = Array.from(container.querySelectorAll("button")).find( (b) => b.textContent === "Cancel", ); expect(btn).toBeTruthy(); btn!.click(); expect(onCancel).toHaveBeenCalled(); }); it("Try again button calls onRetry on timeout", () => { const onRetry = vi.fn(); const { container } = mount(progress("timeout"), { onRetry }); expect(container.textContent).toContain("Couldn’t find your phone"); const btn = Array.from(container.querySelectorAll("button")).find( (b) => b.textContent === "Try again", ); expect(btn).toBeTruthy(); btn!.click(); expect(onRetry).toHaveBeenCalled(); }); it("shows the microphone-permission help text on mic_denied", () => { const { container } = mount(progress("mic_denied")); expect(container.textContent).toContain("Microphone access needed"); expect(container.textContent).toContain("No audio is recorded or stored"); }); it("shows the version-mismatch copy", () => { const { container } = mount(progress("version_mismatch")); expect(container.textContent).toContain("Update Peach on both devices"); }); it("auto-dismisses after 2.5s in the paired state", () => { vi.useFakeTimers(); try { const onDismiss = vi.fn(); mount(progress("paired", { peerDeviceName: "Pixel" }), { onDismiss }); // The createEffect that arms the dismissal runs synchronously on // mount because the phase is already "paired"; advancing the timers // by 2500ms fires the setTimeout callback. vi.advanceTimersByTime(2500); expect(onDismiss).toHaveBeenCalled(); } finally { vi.useRealTimers(); } }); it("data-phase attribute mirrors the progress phase", () => { const { container } = mount(progress("chirping")); const root = container.querySelector(".peach-chirp-pair") as HTMLElement; expect(root.dataset.phase).toBe("chirping"); }); });