/** * useVoiceInput 测试 — mock MediaRecorder 与 ASR client。 */ import { act, renderHook, waitFor } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { useVoiceInput } from "./useVoiceInput"; vi.mock("@/lib/api/audio", () => ({ transcribeAudio: vi.fn().mockResolvedValue({ text: "你好世界" }), })); import { transcribeAudio } from "@/lib/api/audio"; /** 最小 MediaRecorder stub:start/stop 并触发 ondataavailable / onstop。 */ class RecorderStub { state = "inactive"; ondataavailable: ((e: { data: Blob }) => void) | null = null; onstop: (() => void) | null = null; start() { this.state = "recording"; } stop() { this.state = "inactive"; this.ondataavailable?.({ data: new Blob([new Uint8Array([1, 2, 3])]) }); this.onstop?.(); } } describe("useVoiceInput", () => { beforeEach(() => { vi.clearAllMocks(); }); it("toggle 录音并转写后回调 onResult", async () => { const onResult = vi.fn(); const onError = vi.fn(); const stopTracks = vi.fn(); const getTracks = () => [{ stop: stopTracks }] as unknown as MediaStreamTrack[]; Object.defineProperty(globalThis.navigator, "mediaDevices", { value: { getUserMedia: vi.fn().mockResolvedValue({ getTracks }), }, configurable: true, }); // 以 function 构造器作为 MediaRecorder,返回 stub 实例 function MR(this: unknown) { return new RecorderStub(); } Object.defineProperty(globalThis, "MediaRecorder", { value: MR, configurable: true, }); Object.defineProperty(globalThis.MediaRecorder, "isTypeSupported", { value: () => true, configurable: true, }); const { result } = renderHook(() => useVoiceInput({ language: "zh", onResult, onError }), ); expect(result.current.status).toBe("idle"); await act(async () => { await result.current.toggle(); }); expect(result.current.status).toBe("recording"); await act(async () => { await result.current.toggle(); }); await waitFor(() => { expect(result.current.status).toBe("idle"); }); expect(transcribeAudio).toHaveBeenCalledOnce(); expect(transcribeAudio).toHaveBeenCalledWith( expect.any(Blob), expect.stringMatching(/^voice-\d+\.webm$/), "zh", expect.any(AbortSignal), "funasr-sensevoice", ); expect(onResult).toHaveBeenCalledWith("你好世界"); expect(onError).not.toHaveBeenCalled(); expect(stopTracks).toHaveBeenCalled(); }); it("不支持 MediaRecorder 时触发 onError", async () => { const onResult = vi.fn(); const onError = vi.fn(); Object.defineProperty(globalThis.navigator, "mediaDevices", { value: { getUserMedia: vi.fn() }, configurable: true, }); // 设为 undefined 模拟不支持 Object.defineProperty(globalThis, "MediaRecorder", { value: undefined, configurable: true, }); const { result } = renderHook(() => useVoiceInput({ language: "zh", onResult, onError }), ); await act(async () => { await result.current.start(); }); expect(onError).toHaveBeenCalledWith("当前浏览器不支持 MediaRecorder。"); expect(onResult).not.toHaveBeenCalled(); }); });