Files
hunjing/frontend/src/lib/hooks/useVoiceInput.test.ts
T
2026-08-21 16:00:13 +08:00

118 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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();
});
});