From 148fe9659cf3e4dd6747d21ce3520399bc49f6d9 Mon Sep 17 00:00:00 2001 From: Eugene Olonov Date: Tue, 10 Nov 2026 02:54:00 -0800 Subject: [PATCH 1/4] Reject actRecognizeOnce() when an actor throws and await it in hook tests --- __tests__/assets/esm/speech/speechPageObjects.js | 16 +++++++++------- __tests__/html2/hooks/useStartDictate.html | 2 +- __tests__/html2/hooks/useStopDictate.html | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/__tests__/assets/esm/speech/speechPageObjects.js b/__tests__/assets/esm/speech/speechPageObjects.js index 9c171c4a63..fbf992a447 100644 --- a/__tests__/assets/esm/speech/speechPageObjects.js +++ b/__tests__/assets/esm/speech/speechPageObjects.js @@ -83,9 +83,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 +168,7 @@ function actRecognizeOnce(ponyfill, actor, recognizeActor) { end(); }); - spyOn(speechRecognition, 'start').mockImplementationOnce(async () => { + const recognize = async () => { if (typeof recognizeActor === 'string') { start(); audioStart(); @@ -195,15 +194,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}"]`)); From e11ee689341f5bb3f6047ea07f6a6e1dc23dd513 Mon Sep 17 00:00:00 2001 From: Eugene Olonov Date: Tue, 10 Nov 2026 02:58:27 -0800 Subject: [PATCH 2/4] Rethrow speakActor errors from actSpeakOnce() --- __tests__/assets/esm/speech/speechPageObjects.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/__tests__/assets/esm/speech/speechPageObjects.js b/__tests__/assets/esm/speech/speechPageObjects.js index fbf992a447..7981d8eda6 100644 --- a/__tests__/assets/esm/speech/speechPageObjects.js +++ b/__tests__/assets/esm/speech/speechPageObjects.js @@ -43,19 +43,25 @@ function createWebSpeechPonyfill() { */ async function actSpeakOnce({ speechSynthesis }, actor, speakActor) { let lastUtterance; + let spoken; - spyOn(speechSynthesis, 'speak').mockImplementationOnce(async utterance => { + 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 })); + })(); }); await actor(); + // Rethrows an error from speakActor, if speak() was called. + await spoken; + return lastUtterance; } From 3e4cd855bae4eb6d688b69c835009e6a304cf872 Mon Sep 17 00:00:00 2001 From: Eugene Olonov Date: Thu, 12 Nov 2026 15:14:07 -0800 Subject: [PATCH 3/4] Add the changelog fragment --- .changelog/6048-fix-test-speech-helper-errors.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/6048-fix-test-speech-helper-errors.md 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) From 4b446fd914a09d328886dcadf1b0c1d4640c5a6f Mon Sep 17 00:00:00 2001 From: Eugene Olonov Date: Thu, 12 Nov 2026 17:24:42 -0800 Subject: [PATCH 4/4] Fail actSpeakOnce() as soon as its speak callback throws An actor waiting for the utterance's end event never finished when the speak callback threw, so the test hung until timeout with an unhandled rejection. --- __tests__/assets/esm/speech/speechPageObjects.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/__tests__/assets/esm/speech/speechPageObjects.js b/__tests__/assets/esm/speech/speechPageObjects.js index 08c3f5d1a3..aa316ff48b 100644 --- a/__tests__/assets/esm/speech/speechPageObjects.js +++ b/__tests__/assets/esm/speech/speechPageObjects.js @@ -43,8 +43,14 @@ function createWebSpeechPonyfill() { */ async function actSpeakOnce({ speechSynthesis }, actor, speakActor) { let lastUtterance; + let rejectSpeak; let spoken; + // 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; @@ -55,11 +61,13 @@ async function actSpeakOnce({ speechSynthesis }, actor, speakActor) { utterance.dispatchEvent(new SpeechSynthesisEvent('end', { utterance })); })(); + + spoken.catch(rejectSpeak); }); - await actor(); + await Promise.race([actor(), speakFailed]); - // Rethrows an error from speakActor, if speak() was called. + // Waits for speakActor, if speak() was called. await spoken; return lastUtterance;