Bug 1178738 - Have utterances dispatch "canceled" error when they never start. r?smaug draft
authorEitan Isaacson <eitan@monotonous.org>
Wed, 18 May 2016 11:13:45 -0700
changeset 374471 4ff825245d66df4696225136148e7a824ce1e2c4
parent 374075 46f8308c5d7d08f83f31a9a49944ea4685a7664f
child 374472 b04352fe7f621105c8a27627760610753bab0b5c
push id20034
push userbmo:eitan@monotonous.org
push dateThu, 02 Jun 2016 16:08:31 +0000
reviewerssmaug
bugs1178738
milestone49.0a1
Bug 1178738 - Have utterances dispatch "canceled" error when they never start. r?smaug MozReview-Commit-ID: KfJurwcS7mw
dom/media/webspeech/synth/SpeechSynthesis.cpp
dom/media/webspeech/synth/SpeechSynthesis.h
dom/media/webspeech/synth/nsSpeechTask.cpp
dom/media/webspeech/synth/test/common.js
dom/media/webspeech/synth/test/file_global_queue_cancel.html
dom/media/webspeech/synth/test/file_speech_cancel.html
dom/media/webspeech/synth/test/file_speech_cancel_reentry.html
dom/media/webspeech/synth/test/mochitest.ini
dom/media/webspeech/synth/test/test_speech_cancel_reentry.html
--- a/dom/media/webspeech/synth/SpeechSynthesis.cpp
+++ b/dom/media/webspeech/synth/SpeechSynthesis.cpp
@@ -7,16 +7,17 @@
 #include "nsISupportsPrimitives.h"
 #include "nsSpeechTask.h"
 #include "mozilla/Logging.h"
 
 #include "mozilla/dom/ContentChild.h"
 #include "mozilla/dom/Element.h"
 
 #include "mozilla/dom/SpeechSynthesisBinding.h"
+#include "mozilla/dom/SpeechSynthesisErrorEvent.h"
 #include "SpeechSynthesis.h"
 #include "nsSynthVoiceRegistry.h"
 #include "nsIDocument.h"
 
 #undef LOG
 mozilla::LogModule*
 GetSpeechSynthLog()
 {
@@ -189,22 +190,43 @@ SpeechSynthesis::AdvanceQueue()
   if (mCurrentTask) {
     mCurrentTask->SetSpeechSynthesis(this);
   }
 
   return;
 }
 
 void
+SpeechSynthesis::DispatchToCanceledQueue()
+{
+  while (mCanceledQueue.Length()) {
+    RefPtr<SpeechSynthesisUtterance> utterance = mCanceledQueue.ElementAt(0);
+    mCanceledQueue.RemoveElementAt(0);
+
+    utterance->DispatchSpeechSynthesisErrorEvent(
+      0, 0, SpeechSynthesisErrorCode::Canceled);
+  }
+}
+
+void
 SpeechSynthesis::Cancel()
 {
-  mSpeechQueue.Clear();
+  if (mCanceledQueue.IsEmpty()) {
+    mCanceledQueue.SwapElements(mSpeechQueue);
+  } else {
+    while (mSpeechQueue.Length()) {
+      mCanceledQueue.AppendElement(mSpeechQueue.ElementAt(0));
+      mSpeechQueue.RemoveElementAt(0);
+    }
+  }
 
   if (mCurrentTask) {
     mCurrentTask->Cancel();
+  } else {
+    DispatchToCanceledQueue();
   }
 }
 
 void
 SpeechSynthesis::Pause()
 {
   if (Paused()) {
     return;
@@ -234,16 +256,17 @@ SpeechSynthesis::Resume()
 }
 
 void
 SpeechSynthesis::OnEnd(const nsSpeechTask* aTask)
 {
   MOZ_ASSERT(mCurrentTask == aTask);
 
   mCurrentTask = nullptr;
+  DispatchToCanceledQueue();
   AdvanceQueue();
 }
 
 void
 SpeechSynthesis::GetVoices(nsTArray< RefPtr<SpeechSynthesisVoice> >& aResult)
 {
   aResult.Clear();
   uint32_t voiceCount = 0;
--- a/dom/media/webspeech/synth/SpeechSynthesis.h
+++ b/dom/media/webspeech/synth/SpeechSynthesis.h
@@ -65,20 +65,24 @@ public:
 
   IMPL_EVENT_HANDLER(voiceschanged)
 
 private:
   virtual ~SpeechSynthesis();
 
   void AdvanceQueue();
 
+  void DispatchToCanceledQueue();
+
   bool HasVoices() const;
 
   nsTArray<RefPtr<SpeechSynthesisUtterance> > mSpeechQueue;
 
+  nsTArray<RefPtr<SpeechSynthesisUtterance> > mCanceledQueue;
+
   RefPtr<nsSpeechTask> mCurrentTask;
 
   nsRefPtrHashtable<nsStringHashKey, SpeechSynthesisVoice> mVoiceCache;
 
   bool mHoldQueue;
 
   uint64_t mInnerID;
 };
--- a/dom/media/webspeech/synth/nsSpeechTask.cpp
+++ b/dom/media/webspeech/synth/nsSpeechTask.cpp
@@ -530,20 +530,29 @@ nsSpeechTask::DispatchErrorImpl(float aE
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   if (mSpeechSynthesis) {
     mSpeechSynthesis->OnEnd(this);
   }
 
   RefPtr<SpeechSynthesisUtterance> utterance = mUtterance;
-  utterance->mState = (utterance->mState == SpeechSynthesisUtterance::STATE_SPEAKING) ?
-    SpeechSynthesisUtterance::STATE_ENDED : SpeechSynthesisUtterance::STATE_NONE;
-  utterance->DispatchSpeechSynthesisErrorEvent(aCharIndex, aElapsedTime,
-                                               SpeechSynthesisErrorCode(aError));
+  SpeechSynthesisErrorCode err;
+  if (utterance->mState == SpeechSynthesisUtterance::STATE_PENDING &&
+      aError == uint32_t(SpeechSynthesisErrorCode::Interrupted)) {
+    // If utterance never started the error should be "canceled" instead of
+    // "interrupted".
+    err = SpeechSynthesisErrorCode::Canceled;
+    utterance->mState = SpeechSynthesisUtterance::STATE_NONE;
+  } else {
+    err = SpeechSynthesisErrorCode(aError);
+    utterance->mState = SpeechSynthesisUtterance::STATE_ENDED;
+  }
+
+  utterance->DispatchSpeechSynthesisErrorEvent(aCharIndex, aElapsedTime, err);
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsSpeechTask::DispatchBoundary(const nsAString& aName,
                                float aElapsedTime, uint32_t aCharIndex)
 {
--- a/dom/media/webspeech/synth/test/common.js
+++ b/dom/media/webspeech/synth/test/common.js
@@ -84,9 +84,15 @@ function loadSpeechTest(fileName, prefs,
   });
 }
 
 function testSynthState(win, expectedState) {
   for (var attr in expectedState) {
     is(win.speechSynthesis[attr], expectedState[attr],
       win.document.title + ": '" + attr + '" does not match');
   }
+}
+
+function synthEventPromise(utterance, eventType) {
+  return new Promise(resolve => {
+    utterance.addEventListener(eventType, resolve);
+  });
 }
\ No newline at end of file
--- a/dom/media/webspeech/synth/test/file_global_queue_cancel.html
+++ b/dom/media/webspeech/synth/test/file_global_queue_cancel.html
@@ -45,38 +45,50 @@ https://bugzilla.mozilla.org/show_bug.cg
     utterance1.addEventListener('start', function(e) {
       is(eventOrder.shift(), 'start1', 'start1');
       testSynthState(win1, { speaking: true, pending: true });
       testSynthState(win2, { speaking: true, pending: true });
       win2.speechSynthesis.cancel();
       SpecialPowers.wrap(win1.speechSynthesis).forceEnd();
 
     });
-    utterance1.addEventListener('end', function(e) {
+
+    var error4 = synthEventPromise(utterance4, 'error').then(isCanceled);
+    var error5 = synthEventPromise(utterance5, 'error').then(isCanceled);
+
+    var end1 = synthEventPromise(utterance1, 'end').then(() => {
       is(eventOrder.shift(), 'end1', 'end1');
       testSynthState(win1, { pending: true });
       testSynthState(win2, { pending: false });
     });
-    utterance2.addEventListener('start', function(e) {
+
+    var start2 = synthEventPromise(utterance2, 'start').then(() => {
       is(eventOrder.shift(), 'start2', 'start2');
       testSynthState(win1, { speaking: true, pending: true });
       testSynthState(win2, { speaking: true, pending: false });
-      win1.speechSynthesis.cancel();
     });
+
+    Promise.all([error4, error5, end1, start2]).then(
+      () => win1.speechSynthesis.cancel());
+
     utterance2.addEventListener('error', function(e) {
       is(eventOrder.shift(), 'error2', 'error2');
       testSynthState(win1, { speaking: false, pending: false });
       testSynthState(win2, { speaking: false, pending: false });
       SimpleTest.finish();
     });
 
     function wrongUtterance(e) {
       ok(false, 'This shall not be uttered: "' + e.target.text + '"');
     }
 
+    function isCanceled(e) {
+      is(e.error, "canceled", "Utterance was canceled: " + e.target.text);
+    }
+
     utterance3.addEventListener('start', wrongUtterance);
     utterance4.addEventListener('start', wrongUtterance);
     utterance5.addEventListener('start', wrongUtterance);
 
     win1.speechSynthesis.speak(utterance1);
     win1.speechSynthesis.speak(utterance2);
     win1.speechSynthesis.speak(utterance3);
     win2.speechSynthesis.speak(utterance4);
--- a/dom/media/webspeech/synth/test/file_speech_cancel.html
+++ b/dom/media/webspeech/synth/test/file_speech_cancel.html
@@ -39,21 +39,26 @@ function testFunc(done_cb) {
     "consectetur bibendum. Integer a commodo tortor. Duis semper dolor eu" +
     "facilisis facilisis. Etiam venenatis turpis est, quis tincidunt velit" +
     "suscipit a. Cras semper orci in sapien rhoncus bibendum. Suspendisse" +
     "eu ex lobortis, finibus enim in, condimentum quam. Maecenas eget dui" +
     "ipsum. Aliquam tortor leo, interdum eget congue ut, tempor id elit.");
   utterance.addEventListener('start', function(e) {
     ok(true, 'start utterance 1');
     speechSynthesis.cancel();
-    info('cancel!');
-    speechSynthesis.speak(utterance2);
-    info('speak??');
   });
 
+  var utterance_never = new SpeechSynthesisUtterance("I never get uttered");
+  var promise_cancelled = synthEventPromise(utterance_never, "error").then(
+    e => is(e.error, "canceled", "utterance was canceled"));
+  var promise_interrupted = synthEventPromise(utterance, "error").then(
+    e => is(e.error, "interrupted", "utterance was interrupted"));
+  Promise.all([promise_cancelled, promise_interrupted]).then(
+    () => speechSynthesis.speak(utterance2));
+
   var utterance2 = new SpeechSynthesisUtterance("Proin ornare neque vitae " +
     "risus mattis rutrum. Suspendisse a velit ut est convallis aliquet." +
     "Nullam ante elit, malesuada vel luctus rutrum, ultricies nec libero." +
     "Praesent eu iaculis orci. Sed nisl diam, sodales ac purus et," +
     "volutpat interdum tortor. Nullam aliquam porta elit et maximus. Cras" +
     "risus lectus, elementum vel sodales vel, ultricies eget lectus." +
     "Curabitur velit lacus, mollis vel finibus et, molestie sit amet" +
     "sapien. Proin vitae dolor ac augue posuere efficitur ac scelerisque" +
@@ -78,16 +83,17 @@ function testFunc(done_cb) {
   speechSynthesis.pause();
   speechSynthesis.speak(new SpeechSynthesisUtterance("hello."));
   ok(speechSynthesis.pending, "paused speechSynthesis has an utterance queued.");
   speechSynthesis.cancel();
   ok(!speechSynthesis.pending, "paused speechSynthesis has no utterance queued.");
   speechSynthesis.resume();
 
   speechSynthesis.speak(utterance);
+  speechSynthesis.speak(utterance_never);
   ok(!speechSynthesis.speaking, "speechSynthesis is not speaking yet.");
   ok(speechSynthesis.pending, "speechSynthesis has an utterance queued.");
 }
 
 // Run test with no global queue, and then run it with a global queue.
 testFunc(function() {
   SpecialPowers.pushPrefEnv(
     { set: [['media.webspeech.synth.force_global_queue', true]] }, function() {
new file mode 100644
--- /dev/null
+++ b/dom/media/webspeech/synth/test/file_speech_cancel_reentry.html
@@ -0,0 +1,87 @@
+<!DOCTYPE HTML>
+<html lang="it-IT-noend">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1178738
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 1178738: Make sure cancel is reentrant</title>
+  <script type="application/javascript">
+    window.SimpleTest = parent.SimpleTest;
+    window.info = parent.info;
+    window.is = parent.is;
+    window.isnot = parent.isnot;
+    window.ok = parent.ok;
+  </script>
+  <script type="application/javascript" src="common.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1178738">Mozilla Bug 1178738</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1178738 **/
+
+function testFunc(done_cb) {
+  var utterance = new SpeechSynthesisUtterance("utterance 1");
+  var utterance2 = new SpeechSynthesisUtterance("utterance 2");
+  var utterance3 = new SpeechSynthesisUtterance("utterance 3");
+  var utterance4 = new SpeechSynthesisUtterance("utterance 4");
+  var utterance5 = new SpeechSynthesisUtterance("utterance 5");
+  utterance5.lang = "en-GB";
+
+  utterance.addEventListener('start', function(e) {
+    ok(true, 'start utterance 1');
+    speechSynthesis.cancel();
+  });
+
+
+  utterance.addEventListener('error', function(e) {
+    is(e.error, "interrupted", "utterance was interrupted");
+  });
+
+  utterance2.addEventListener("error", function(e) {
+    is(e.error, "canceled", "utterance was canceled");
+    speechSynthesis.cancel();
+    speechSynthesis.speak(utterance3);
+  });
+
+  utterance2.addEventListener("start", function(e) {
+    ok(false, "Utterance should never be spoken")
+  });
+
+  utterance3.addEventListener('start', function(e) {
+    ok(true, 'start utterance 3');
+    speechSynthesis.speak(utterance4);
+    speechSynthesis.cancel();
+  });
+
+  utterance3.addEventListener('error', function(e) {
+    is(e.error, "interrupted", "utterance was interrupted");
+    speechSynthesis.cancel();
+    speechSynthesis.speak(utterance5);
+  });
+
+  var utterance4_error = synthEventPromise(utterance4, "error").then(
+    e => is(e.error, "canceled", "utterance 4 was canceled"));
+  var utterance5_end = synthEventPromise(utterance5, "end").then(
+    e => ok(true, "utterance 5 ended"));
+
+  Promise.all([utterance4_error, utterance5_end]).then(done_cb);
+
+
+  speechSynthesis.speak(utterance);
+  speechSynthesis.speak(utterance2);
+}
+
+// Run test with no global queue, and then run it with a global queue.
+testFunc(SimpleTest.finish);
+
+</script>
+</pre>
+</body>
+</html>
--- a/dom/media/webspeech/synth/test/mochitest.ini
+++ b/dom/media/webspeech/synth/test/mochitest.ini
@@ -3,24 +3,26 @@ tags=msg
 subsuite = media
 support-files =
   common.js
   file_bfcache_frame.html
   file_setup.html
   file_speech_queue.html
   file_speech_simple.html
   file_speech_cancel.html
+  file_speech_cancel_reentry.html
   file_speech_error.html
   file_indirect_service_events.html
   file_global_queue.html
   file_global_queue_cancel.html
   file_global_queue_pause.html
 
 [test_setup.html]
 [test_speech_queue.html]
 [test_speech_simple.html]
 [test_speech_cancel.html]
+[test_speech_cancel_reentry.html]
 [test_speech_error.html]
 [test_indirect_service_events.html]
 [test_global_queue.html]
 [test_global_queue_cancel.html]
 [test_global_queue_pause.html]
 [test_bfcache.html]
new file mode 100644
--- /dev/null
+++ b/dom/media/webspeech/synth/test/test_speech_cancel_reentry.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1178738
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 1178738: Make sure cancel is reentrant</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="common.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1178738">Mozilla Bug 1178738</a>
+<p id="display"></p>
+<iframe id="testFrame"></iframe>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1178738 **/
+
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv(
+  { set: [['media.webspeech.synth.enabled', true],
+          ['media.webspeech.synth.force_global_queue', false]] },
+  function() { loadSpeechTest("file_speech_cancel_reentry.html"); });
+
+</script>
+</pre>
+</body>
+</html>