123import { afterEach, describe, expect, it, vi } from "vitest";4import { ChirpPairingFlow } from "./ChirpPairingFlow";5import type { PairingProgress } from "../../shared/sync-types";6import { renderView } from "../views/view-test-utils";78function progress(phase: PairingProgress["phase"], extra: Partial<PairingProgress> = {}): PairingProgress {9 return { phase, ...extra };10}1112describe("ChirpPairingFlow", () => {13 const cleanups: Array<() => void> = [];14 afterEach(() => {15 while (cleanups.length) cleanups.pop()!();16 });1718 function mount(progress: PairingProgress, handlers: { onCancel?: () => void; onRetry?: () => void; onDismiss?: () => void } = {}) {19 const r = renderView(20 <ChirpPairingFlow21 progress={progress}22 onCancel={handlers.onCancel ?? (() => {})}23 onRetry={handlers.onRetry ?? (() => {})}24 onDismiss={handlers.onDismiss ?? (() => {})}25 />,26 );27 cleanups.push(r.dispose);28 return r;29 }3031 it("renders the listening copy in Act 1", () => {32 const { container } = mount(progress("listening"));33 expect(container.textContent).toContain("Hold your phone near your laptop");34 expect(container.textContent).toContain("Peach will pair automatically");35 });3637 it("renders the handshake copy in Act 2", () => {38 const { container } = mount(progress("handshake"));39 expect(container.textContent).toContain("Pairing…");40 });4142 it("renders the paired copy with the peer device name", () => {43 const { container } = mount(progress("paired", { peerDeviceName: "June's Pixel 9" }));44 expect(container.textContent).toContain("Paired!");45 expect(container.textContent).toContain("Connected to June's Pixel 9");46 });4748 it("Cancel button calls onCancel in the listening state", () => {49 const onCancel = vi.fn();50 const { container } = mount(progress("listening"), { onCancel });51 const btn = Array.from(container.querySelectorAll("button")).find(52 (b) => b.textContent === "Cancel",53 );54 expect(btn).toBeTruthy();55 btn!.click();56 expect(onCancel).toHaveBeenCalled();57 });5859 it("Try again button calls onRetry on timeout", () => {60 const onRetry = vi.fn();61 const { container } = mount(progress("timeout"), { onRetry });62 expect(container.textContent).toContain("Couldn’t find your phone");63 const btn = Array.from(container.querySelectorAll("button")).find(64 (b) => b.textContent === "Try again",65 );66 expect(btn).toBeTruthy();67 btn!.click();68 expect(onRetry).toHaveBeenCalled();69 });7071 it("shows the microphone-permission help text on mic_denied", () => {72 const { container } = mount(progress("mic_denied"));73 expect(container.textContent).toContain("Microphone access needed");74 expect(container.textContent).toContain("No audio is recorded or stored");75 });7677 it("shows the version-mismatch copy", () => {78 const { container } = mount(progress("version_mismatch"));79 expect(container.textContent).toContain("Update Peach on both devices");80 });8182 it("auto-dismisses after 2.5s in the paired state", () => {83 vi.useFakeTimers();84 try {85 const onDismiss = vi.fn();86 mount(progress("paired", { peerDeviceName: "Pixel" }), { onDismiss });87 88 89 90 vi.advanceTimersByTime(2500);91 expect(onDismiss).toHaveBeenCalled();92 } finally {93 vi.useRealTimers();94 }95 });9697 it("data-phase attribute mirrors the progress phase", () => {98 const { container } = mount(progress("chirping"));99 const root = container.querySelector(".peach-chirp-pair") as HTMLElement;100 expect(root.dataset.phase).toBe("chirping");101 });102});