Bug 1178738 - Have utterances dispatch "canceled" error when they never start. r=smaug
☠☠ backed out by f19dab43cca7 ☠ ☠
authorEitan Isaacson <eitan@monotonous.org>
Wed, 18 May 2016 11:13:45 -0700
changeset 300192 5c995bbdfa3ac09234e829d5a8ba91741c89ff35
parent 300191 ab50796d261607416f6c9b785015d6601edaa348
child 300193 54d1e8caf69b2ffe033c4664b5651a1f7987ecb5
push id77841
push usereisaacson@mozilla.com
push dateThu, 02 Jun 2016 16:10:29 +0000
treeherdermozilla-inbound@54d1e8caf69b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1178738
milestone49.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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>