diff --git a/src/components/VoiceSearch/__tests__/VoiceSearch-test.tsx b/src/components/VoiceSearch/__tests__/VoiceSearch-test.tsx index 583a4513dc..6d5983c8aa 100644 --- a/src/components/VoiceSearch/__tests__/VoiceSearch-test.tsx +++ b/src/components/VoiceSearch/__tests__/VoiceSearch-test.tsx @@ -76,7 +76,7 @@ describe('VoiceSearch', () => { }); it('with custom template for status', () => { - const props = { + const props: VoiceSearchProps = { ...defaultProps, isListening: true, voiceListeningState: { diff --git a/src/lib/voiceSearchHelper/__tests__/index-test.ts b/src/lib/voiceSearchHelper/__tests__/index-test.ts index 6aea11d65d..b98d1017f2 100644 --- a/src/lib/voiceSearchHelper/__tests__/index-test.ts +++ b/src/lib/voiceSearchHelper/__tests__/index-test.ts @@ -191,4 +191,19 @@ describe('VoiceSearchHelper', () => { voiceSearchHelper.dispose(); expect(stop).toHaveBeenCalledTimes(1); }); + + it('stops and the status becomes `finished`', () => { + window.SpeechRecognition = createFakeSpeechRecognition(); + const onQueryChange = (): void => {}; + const onStateChange = (): void => {}; + const voiceSearchHelper = createVoiceSearchHelper({ + searchAsYouSpeak: true, + onQueryChange, + onStateChange, + }); + + voiceSearchHelper.toggleListening(); + voiceSearchHelper.toggleListening(); + expect(voiceSearchHelper.getState().status).toBe('finished'); + }); }); diff --git a/src/lib/voiceSearchHelper/index.ts b/src/lib/voiceSearchHelper/index.ts index 56911d6a5e..1a5fc72a90 100644 --- a/src/lib/voiceSearchHelper/index.ts +++ b/src/lib/voiceSearchHelper/index.ts @@ -1,21 +1,22 @@ -const STATUS_INITIAL = 'initial'; -const STATUS_ASKING_PERMISSION = 'askingPermission'; -const STATUS_WAITING = 'waiting'; -const STATUS_RECOGNIZING = 'recognizing'; -const STATUS_FINISHED = 'finished'; -const STATUS_ERROR = 'error'; - export type VoiceSearchHelperParams = { searchAsYouSpeak: boolean; onQueryChange: (query: string) => void; onStateChange: () => void; }; +export type Status = + | 'initial' + | 'askingPermission' + | 'waiting' + | 'recognizing' + | 'finished' + | 'error'; + export type VoiceListeningState = { - status: string; + status: Status; transcript: string; isSpeechFinal: boolean; - errorCode?: string; + errorCode?: SpeechRecognitionErrorCode; }; export type VoiceSearchHelper = { @@ -36,46 +37,46 @@ export default function createVoiceSearchHelper({ const SpeechRecognitionAPI: new () => SpeechRecognition = (window as any).webkitSpeechRecognition || (window as any).SpeechRecognition; - const getDefaultState = (status: string): VoiceListeningState => ({ + const getDefaultState = (status: Status): VoiceListeningState => ({ status, transcript: '', isSpeechFinal: false, errorCode: undefined, }); - let state: VoiceListeningState = getDefaultState(STATUS_INITIAL); + let state: VoiceListeningState = getDefaultState('initial'); let recognition: SpeechRecognition | undefined; const isBrowserSupported = (): boolean => Boolean(SpeechRecognitionAPI); const isListening = (): boolean => - state.status === STATUS_ASKING_PERMISSION || - state.status === STATUS_WAITING || - state.status === STATUS_RECOGNIZING; + state.status === 'askingPermission' || + state.status === 'waiting' || + state.status === 'recognizing'; - const setState = (newState = {}): void => { + const setState = (newState: Partial = {}): void => { state = { ...state, ...newState }; onStateChange(); }; const getState = (): VoiceListeningState => state; - const resetState = (status = STATUS_INITIAL): void => { + const resetState = (status: Status = 'initial'): void => { setState(getDefaultState(status)); }; const onStart = (): void => { setState({ - status: STATUS_WAITING, + status: 'waiting', }); }; const onError = (event: SpeechRecognitionError): void => { - setState({ status: STATUS_ERROR, errorCode: event.error }); + setState({ status: 'error', errorCode: event.error }); }; const onResult = (event: SpeechRecognitionEvent): void => { setState({ - status: STATUS_RECOGNIZING, + status: 'recognizing', transcript: (event.results[0] && event.results[0][0] && @@ -92,25 +93,17 @@ export default function createVoiceSearchHelper({ if (!state.errorCode && state.transcript && !searchAsYouSpeak) { onQueryChange(state.transcript); } - if (state.status !== STATUS_ERROR) { - setState({ status: STATUS_FINISHED }); + if (state.status !== 'error') { + setState({ status: 'finished' }); } }; - const stop = (): void => { - if (recognition) { - recognition.stop(); - recognition = undefined; - } - resetState(); - }; - const start = (): void => { recognition = new SpeechRecognitionAPI(); if (!recognition) { return; } - resetState(STATUS_ASKING_PERMISSION); + resetState('askingPermission'); recognition.interimResults = true; recognition.addEventListener('start', onStart); recognition.addEventListener('error', onError); @@ -131,6 +124,14 @@ export default function createVoiceSearchHelper({ recognition = undefined; }; + const stop = (): void => { + dispose(); + // Because `dispose` removes event listeners, `end` listener is not called. + // So we're setting the `status` as `finished` here. + // If we don't do it, it will be still `waiting` or `recognizing`. + resetState('finished'); + }; + const toggleListening = (): void => { if (!isBrowserSupported()) { return;