118 lines
3.3 KiB
TypeScript
118 lines
3.3 KiB
TypeScript
/**
|
||
* 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();
|
||
});
|
||
});
|