ChirpPairingFlow.test.tsx

3.8 KB · raw

1// @vitest-environment jsdom23import { 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      // The createEffect that arms the dismissal runs synchronously on88      // mount because the phase is already "paired"; advancing the timers89      // by 2500ms fires the setTimeout callback.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});