test(webui): cover xiaomi mimo wav recording path
This commit is contained in:
@@ -226,7 +226,20 @@ function mockVoiceRecorder(blob = new Blob(["voice"], { type: "audio/webm" })) {
|
||||
return { getUserMedia, stopTrack };
|
||||
}
|
||||
|
||||
function mockVoiceAudioInput(sample = 128, state: AudioContextState = "running") {
|
||||
function mockVoiceAudioInput(
|
||||
sample = 128,
|
||||
state: AudioContextState = "running",
|
||||
decodedChannels?: Float32Array[],
|
||||
) {
|
||||
const decodeAudioDataMock = vi.fn(async () => {
|
||||
if (!decodedChannels) throw new Error("decodeAudioData not mocked");
|
||||
return {
|
||||
numberOfChannels: decodedChannels.length,
|
||||
sampleRate: 16_000,
|
||||
getChannelData: (channel: number) => decodedChannels[channel],
|
||||
} as AudioBuffer;
|
||||
});
|
||||
|
||||
class FakeAudioContext {
|
||||
state = state;
|
||||
|
||||
@@ -244,6 +257,7 @@ function mockVoiceAudioInput(sample = 128, state: AudioContextState = "running")
|
||||
}
|
||||
|
||||
close = vi.fn(async () => undefined);
|
||||
decodeAudioData = decodeAudioDataMock;
|
||||
resume = vi.fn(async () => undefined);
|
||||
}
|
||||
|
||||
@@ -254,6 +268,7 @@ function mockVoiceAudioInput(sample = 128, state: AudioContextState = "running")
|
||||
vi.spyOn(window, "cancelAnimationFrame").mockImplementation((id) =>
|
||||
window.clearTimeout(id as unknown as number)
|
||||
);
|
||||
return { decodeAudioData: decodeAudioDataMock };
|
||||
}
|
||||
|
||||
async function waitForVoiceCapture(): Promise<void> {
|
||||
@@ -262,6 +277,15 @@ async function waitForVoiceCapture(): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
function bytesFromDataUrl(dataUrl: string): Uint8Array {
|
||||
const [, base64 = ""] = dataUrl.split(",");
|
||||
return Uint8Array.from(atob(base64), (char) => char.charCodeAt(0));
|
||||
}
|
||||
|
||||
function ascii(bytes: Uint8Array, offset: number, length: number): string {
|
||||
return String.fromCharCode(...bytes.slice(offset, offset + length));
|
||||
}
|
||||
|
||||
describe("ThreadComposer", () => {
|
||||
it("renders a readonly hero model composer when provided", () => {
|
||||
render(
|
||||
@@ -337,6 +361,47 @@ describe("ThreadComposer", () => {
|
||||
expect(onSend).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("converts voice recordings to wav for Xiaomi MiMo transcription", async () => {
|
||||
mockVoiceRecorder(new Blob([new Uint8Array([1, 2, 3, 4])], { type: "audio/webm" }));
|
||||
const { decodeAudioData } = mockVoiceAudioInput(
|
||||
180,
|
||||
"running",
|
||||
[new Float32Array([0, 0.5, -0.5])],
|
||||
);
|
||||
const onTranscribeAudio = vi.fn(async () => "mimo voice");
|
||||
render(
|
||||
<ThreadComposer
|
||||
onSend={vi.fn()}
|
||||
onTranscribeAudio={onTranscribeAudio}
|
||||
placeholder="Type your message..."
|
||||
transcriptionProvider="xiaomi_mimo"
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Voice input" }));
|
||||
expect(await screen.findByLabelText("Recording 0:00")).toBeInTheDocument();
|
||||
await waitForVoiceCapture();
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Stop recording" }));
|
||||
|
||||
await waitFor(() => expect(onTranscribeAudio).toHaveBeenCalledTimes(1));
|
||||
const [dataUrl, options] = onTranscribeAudio.mock.calls[0];
|
||||
expect(dataUrl).toMatch(/^data:audio\/wav;base64,/);
|
||||
expect(options).toEqual(expect.objectContaining({ durationMs: expect.any(Number) }));
|
||||
expect(decodeAudioData).toHaveBeenCalledTimes(1);
|
||||
|
||||
const bytes = bytesFromDataUrl(dataUrl);
|
||||
const view = new DataView(bytes.buffer);
|
||||
expect(ascii(bytes, 0, 4)).toBe("RIFF");
|
||||
expect(ascii(bytes, 8, 4)).toBe("WAVE");
|
||||
expect(ascii(bytes, 12, 4)).toBe("fmt ");
|
||||
expect(view.getUint16(20, true)).toBe(1);
|
||||
expect(view.getUint16(22, true)).toBe(1);
|
||||
expect(view.getUint32(24, true)).toBe(16_000);
|
||||
expect(view.getUint16(34, true)).toBe(16);
|
||||
expect(ascii(bytes, 36, 4)).toBe("data");
|
||||
await waitFor(() => expect(screen.getByLabelText("Message input")).toHaveValue("mimo voice"));
|
||||
});
|
||||
|
||||
it("does not start duplicate voice recordings while microphone access is pending", async () => {
|
||||
const { getUserMedia, stopTrack } = mockVoiceRecorder();
|
||||
let resolveStream: ((stream: MediaStream) => void) | undefined;
|
||||
|
||||
Reference in New Issue
Block a user