diff --git a/.changelog/6048-fix-test-speech-helper-errors.md b/.changelog/6048-fix-test-speech-helper-errors.md new file mode 100644 index 0000000000..93b09a49b5 --- /dev/null +++ b/.changelog/6048-fix-test-speech-helper-errors.md @@ -0,0 +1,3 @@ +### Fixed + +- 👷🏻 Fixed speech test helpers not failing the test when its callback throws, in PR [#6048](https://github.com/microsoft/BotFramework-WebChat/pull/6048), by [@OEvgeny](https://github.com/OEvgeny) diff --git a/__tests__/assets/esm/speech/speechPageObjects.js b/__tests__/assets/esm/speech/speechPageObjects.js index 45d05f623b..aa316ff48b 100644 --- a/__tests__/assets/esm/speech/speechPageObjects.js +++ b/__tests__/assets/esm/speech/speechPageObjects.js @@ -43,18 +43,32 @@ function createWebSpeechPonyfill() { */ async function actSpeakOnce({ speechSynthesis }, actor, speakActor) { let lastUtterance; + let rejectSpeak; + let spoken; - spyOn(speechSynthesis, 'speak').mockImplementationOnce(async utterance => { + // Rejects as soon as speakActor throws, so an actor waiting for the utterance's "end" event does not hang. + const speakFailed = new Promise((_, reject) => { + rejectSpeak = reject; + }); + + spyOn(speechSynthesis, 'speak').mockImplementationOnce(utterance => { lastUtterance = utterance; - utterance.dispatchEvent(new SpeechSynthesisEvent('start', { utterance })); + spoken = (async () => { + utterance.dispatchEvent(new SpeechSynthesisEvent('start', { utterance })); - await speakActor?.(utterance); + await speakActor?.(utterance); - utterance.dispatchEvent(new SpeechSynthesisEvent('end', { utterance })); + utterance.dispatchEvent(new SpeechSynthesisEvent('end', { utterance })); + })(); + + spoken.catch(rejectSpeak); }); - await actor(); + await Promise.race([actor(), speakFailed]); + + // Waits for speakActor, if speak() was called. + await spoken; return lastUtterance; } @@ -83,9 +97,8 @@ async function actSpeak({ speechSynthesis }, actor, speakActor) { return Object.freeze(utterances); } -function actRecognizeOnce(ponyfill, actor, recognizeActor) { - // eslint-disable-next-line no-async-promise-executor - return new Promise(async resolve => { +async function actRecognizeOnce(ponyfill, actor, recognizeActor) { + const recognized = new Promise((resolve, reject) => { const OriginalSpeechRecognition = ponyfill.SpeechRecognition; spyOn(ponyfill, 'SpeechRecognition').mockImplementationOnce(() => { @@ -169,7 +182,7 @@ function actRecognizeOnce(ponyfill, actor, recognizeActor) { end(); }); - spyOn(speechRecognition, 'start').mockImplementationOnce(async () => { + const recognize = async () => { // Like a browser, events come after start() returned and "starting" is painted: start() is called from an effect, // events inside it would render "starting" and "dictating" in the same frame on React 18 and later. await new Promise(resolve => requestAnimationFrame(() => setTimeout(resolve, 0))); @@ -199,15 +212,18 @@ function actRecognizeOnce(ponyfill, actor, recognizeActor) { finalizedResult }); } + }; - resolve(); + spyOn(speechRecognition, 'start').mockImplementationOnce(() => { + recognize().then(resolve, reject); }); return speechRecognition; }); - - await actor(); }); + + // Waits for both actors, so an error thrown by either one rejects. + await Promise.all([actor(), recognized]); } async function sendMessageViaMicrophone(speechPonyfill, text) { diff --git a/__tests__/html2/hooks/useStartDictate.html b/__tests__/html2/hooks/useStartDictate.html index 92f8d11f4a..75f3a763b2 100644 --- a/__tests__/html2/hooks/useStartDictate.html +++ b/__tests__/html2/hooks/useStartDictate.html @@ -55,7 +55,7 @@ `[data-testid="${testIds.sendBoxMicrophoneButton}"]` )?.parentElement; - actRecognizeOnce( + await actRecognizeOnce( ponyfill, async () => { startDictate(); diff --git a/__tests__/html2/hooks/useStopDictate.html b/__tests__/html2/hooks/useStopDictate.html index 1c35745e0b..4d613d83dd 100644 --- a/__tests__/html2/hooks/useStopDictate.html +++ b/__tests__/html2/hooks/useStopDictate.html @@ -51,7 +51,7 @@ await pageConditions.uiConnected(); - actRecognizeOnce( + await actRecognizeOnce( ponyfill, async () => { await host.click(document.querySelector(`[data-testid="${testIds.sendBoxMicrophoneButton}"]`));