mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-07 01:48:53 +00:00
test(webui): cover xiaomi mimo wav recording path
This commit is contained in:
parent
28c8c89a42
commit
4636c78100
@ -226,7 +226,20 @@ function mockVoiceRecorder(blob = new Blob(["voice"], { type: "audio/webm" })) {
|
|||||||
return { getUserMedia, stopTrack };
|
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 {
|
class FakeAudioContext {
|
||||||
state = state;
|
state = state;
|
||||||
|
|
||||||
@ -244,6 +257,7 @@ function mockVoiceAudioInput(sample = 128, state: AudioContextState = "running")
|
|||||||
}
|
}
|
||||||
|
|
||||||
close = vi.fn(async () => undefined);
|
close = vi.fn(async () => undefined);
|
||||||
|
decodeAudioData = decodeAudioDataMock;
|
||||||
resume = vi.fn(async () => undefined);
|
resume = vi.fn(async () => undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,6 +268,7 @@ function mockVoiceAudioInput(sample = 128, state: AudioContextState = "running")
|
|||||||
vi.spyOn(window, "cancelAnimationFrame").mockImplementation((id) =>
|
vi.spyOn(window, "cancelAnimationFrame").mockImplementation((id) =>
|
||||||
window.clearTimeout(id as unknown as number)
|
window.clearTimeout(id as unknown as number)
|
||||||
);
|
);
|
||||||
|
return { decodeAudioData: decodeAudioDataMock };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitForVoiceCapture(): Promise<void> {
|
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", () => {
|
describe("ThreadComposer", () => {
|
||||||
it("renders a readonly hero model composer when provided", () => {
|
it("renders a readonly hero model composer when provided", () => {
|
||||||
render(
|
render(
|
||||||
@ -337,6 +361,47 @@ describe("ThreadComposer", () => {
|
|||||||
expect(onSend).not.toHaveBeenCalled();
|
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 () => {
|
it("does not start duplicate voice recordings while microphone access is pending", async () => {
|
||||||
const { getUserMedia, stopTrack } = mockVoiceRecorder();
|
const { getUserMedia, stopTrack } = mockVoiceRecorder();
|
||||||
let resolveStream: ((stream: MediaStream) => void) | undefined;
|
let resolveStream: ((stream: MediaStream) => void) | undefined;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user