Files
deer-flow/frontend/tests/unit/core/voice-input/speech-recognition.test.ts
T
Ryker_FengandGitHub be637163a6 feat(frontend): add voice dictation input (#4036)
* feat(frontend): add voice dictation input

* fix(frontend): address voice input review feedback

* ci: rerun frontend checks
2026-07-10 23:13:06 +08:00

121 lines
4.1 KiB
TypeScript

import { describe, expect, it } from "@rstest/core";
import {
appendSpeechTranscript,
getSpeechRecognitionConstructor,
getSpeechRecognitionLanguage,
mapSpeechRecognitionError,
readSpeechRecognitionTranscript,
shouldRestartSpeechRecognition,
type SpeechRecognitionConstructor,
} from "@/core/voice-input/speech-recognition";
describe("speech recognition helpers", () => {
it("prefers the standard constructor and falls back to webkit", () => {
const standard = makeSpeechRecognitionConstructor();
const webkit = makeSpeechRecognitionConstructor();
expect(
getSpeechRecognitionConstructor({
SpeechRecognition: standard as unknown as SpeechRecognitionConstructor,
webkitSpeechRecognition:
webkit as unknown as SpeechRecognitionConstructor,
}),
).toBe(standard);
expect(
getSpeechRecognitionConstructor({
webkitSpeechRecognition:
webkit as unknown as SpeechRecognitionConstructor,
}),
).toBe(webkit);
expect(getSpeechRecognitionConstructor({})).toBeNull();
});
it("maps DeerFlow locales to browser recognition locales", () => {
expect(getSpeechRecognitionLanguage("zh-CN")).toBe("zh-CN");
expect(getSpeechRecognitionLanguage("zh-Hans")).toBe("zh-CN");
expect(getSpeechRecognitionLanguage("en-US")).toBe("en-US");
expect(getSpeechRecognitionLanguage("en-GB")).toBe("en-GB");
expect(getSpeechRecognitionLanguage("fr-FR")).toBe("fr-FR");
expect(getSpeechRecognitionLanguage("ja-JP")).toBe("ja-JP");
expect(getSpeechRecognitionLanguage("xx-YY")).toBe("en-US");
expect(getSpeechRecognitionLanguage("not a locale")).toBe("en-US");
});
it("combines final and interim transcripts with whitespace cleanup", () => {
expect(
readSpeechRecognitionTranscript({
length: 3,
0: { isFinal: true, length: 1, 0: { transcript: " hello " } },
1: { isFinal: true, length: 1, 0: { transcript: " world" } },
2: { isFinal: false, length: 1, 0: { transcript: " again " } },
}),
).toEqual({
finalText: "hello world",
interimText: "again",
text: "hello world again",
});
});
it("appends transcript to an existing draft without duplicating whitespace", () => {
expect(appendSpeechTranscript("", " hello world ")).toBe("hello world");
expect(appendSpeechTranscript("Draft", "voice text")).toBe(
"Draft voice text",
);
expect(appendSpeechTranscript("Draft\n", "voice text")).toBe(
"Draft voice text",
);
expect(appendSpeechTranscript("Draft", " ")).toBe("Draft");
});
it("normalizes browser speech recognition errors", () => {
expect(mapSpeechRecognitionError("not-allowed")).toBe("permission_denied");
expect(mapSpeechRecognitionError("service-not-allowed")).toBe(
"permission_denied",
);
expect(mapSpeechRecognitionError("audio-capture")).toBe(
"microphone_unavailable",
);
expect(mapSpeechRecognitionError("language-not-supported")).toBe(
"unsupported_language",
);
expect(mapSpeechRecognitionError("network")).toBe("network");
expect(mapSpeechRecognitionError("no-speech")).toBe("no_speech");
expect(mapSpeechRecognitionError("aborted")).toBe("cancelled");
expect(mapSpeechRecognitionError("bad-grammar")).toBe("unknown");
});
it("restarts only after browser auto-end conditions", () => {
expect(shouldRestartSpeechRecognition(null)).toBe(true);
expect(shouldRestartSpeechRecognition("no_speech")).toBe(true);
expect(shouldRestartSpeechRecognition("cancelled")).toBe(false);
expect(shouldRestartSpeechRecognition("permission_denied")).toBe(false);
expect(shouldRestartSpeechRecognition("network")).toBe(false);
expect(shouldRestartSpeechRecognition("unknown")).toBe(false);
});
});
function makeSpeechRecognitionConstructor(): SpeechRecognitionConstructor {
return class {
continuous = false;
interimResults = false;
lang = "";
maxAlternatives = 1;
onend = null;
onerror = null;
onresult = null;
start() {
return undefined;
}
stop() {
return undefined;
}
abort() {
return undefined;
}
};
}