Bug 1162699 - Replace mochitest test synth services with global services to simplify tests. r=smaug
authorEitan Isaacson <eitan@monotonous.org>
Thu, 14 May 2015 16:24:14 -0700
changeset 244001 4f320bb16ae4657356a2077bf2eed515806bbe50
parent 244000 5f8235c2f2a3eb0ccd590ec90c7e3b1db25f7fac
child 244002 e43f6dc95fc5f3bbd31a71c147d606c1d0b215ec
push id28762
push usercbook@mozilla.com
push dateFri, 15 May 2015 15:40:04 +0000
treeherdermozilla-central@1a8343f8ed83 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1162699
milestone41.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 1162699 - Replace mochitest test synth services with global services to simplify tests. r=smaug Also, remove ipc tests since we now can enable these tests in e10s. Also, make utterances very long in cancel test so that we actually interrupt them.
dom/media/webspeech/synth/ipc/test/file_ipc.html
dom/media/webspeech/synth/ipc/test/mochitest.ini
dom/media/webspeech/synth/ipc/test/test_ipc.html
dom/media/webspeech/synth/moz.build
dom/media/webspeech/synth/nsSpeechTask.cpp
dom/media/webspeech/synth/test/FakeSynthModule.cpp
dom/media/webspeech/synth/test/common.js
dom/media/webspeech/synth/test/file_indirect_service_events.html
dom/media/webspeech/synth/test/file_setup.html
dom/media/webspeech/synth/test/file_speech_cancel.html
dom/media/webspeech/synth/test/file_speech_queue.html
dom/media/webspeech/synth/test/file_speech_simple.html
dom/media/webspeech/synth/test/mochitest.ini
dom/media/webspeech/synth/test/nsFakeSynthServices.cpp
dom/media/webspeech/synth/test/nsFakeSynthServices.h
deleted file mode 100644
--- a/dom/media/webspeech/synth/ipc/test/file_ipc.html
+++ /dev/null
@@ -1,196 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<head>
-  <title>Test for OOP TTS</title>
-  <script type="application/javascript" src="../../test/common.js"></script>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-</head>
-  <body>
-
-  <script type="application/javascript;version=1.7">
-    "use strict";
-
-    window.SimpleTest = parent.SimpleTest;
-    window.ok = parent.ok;
-    window.is = parent.is;
-    window.info = parent.info;
-
-    // The crash observer registration functions are stubbed out here to
-    // prevent the iframe test runner from breaking later crash-related tests.
-    function iframeScriptFirst() {
-      SpecialPowers.prototype.registerProcessCrashObservers = () => {};
-      SpecialPowers.prototype.unregisterProcessCrashObservers = () => {};
-
-      content.wrappedJSObject.RunSet.reloadAndRunAll({
-          preventDefault: function() { },
-          __exposedProps__: { preventDefault: 'r' }
-      });
-    }
-
-    function iframeScriptSecond() {
-      let TestRunner = content.wrappedJSObject.TestRunner;
-
-      let oldComplete = TestRunner.onComplete;
-
-      TestRunner.onComplete = function() {
-        TestRunner.onComplete = oldComplete;
-
-        sendAsyncMessage("test:SpeechSynthesis:ipcTestComplete", {
-          result: JSON.stringify(TestRunner._failedTests)
-        });
-
-        if (oldComplete) {
-          oldComplete();
-        }
-      };
-      TestRunner.structuredLogger._dumpMessage = function(msg) {
-        sendAsyncMessage("test:SpeechSynthesis:ipcTestMessage", { msg: msg });
-      }
-    }
-
-    let VALID_ACTIONS = ['suite_start', 'suite_end', 'test_start', 'test_end', 'test_status', 'process_output', 'log'];
-    function validStructuredMessage(message) {
-      return message.action !== undefined && VALID_ACTIONS.indexOf(message.action) >= 0;
-    }
-    function onTestMessage(data) {
-      let message = SpecialPowers.wrap(data).data.msg;
-
-      if (validStructuredMessage(message)) {
-        switch (message.action) {
-          case "test_status":
-          case "test_end":
-            let test_tokens = message.test.split("/");
-            let test_name = test_tokens[test_tokens.length - 1];
-            if (message.subtest) {
-                test_name += " | " + message.subtest;
-            }
-            ok(message.expected === undefined, test_name, message.message);
-            break;
-          case "log":
-            info(message.message);
-            break;
-          default:
-            // nothing
-        }
-      }
-    }
-
-    function onTestComplete() {
-      let comp = SpecialPowers.wrap(SpecialPowers.Components);
-      let mm = SpecialPowers.getBrowserFrameMessageManager(iframe);
-      let spObserver = comp.classes["@mozilla.org/special-powers-observer;1"]
-                            .getService(comp.interfaces.nsIMessageListener);
-
-      mm.removeMessageListener("SPPrefService", spObserver);
-      mm.removeMessageListener("SPProcessCrashService", spObserver);
-      mm.removeMessageListener("SPPingService", spObserver);
-      mm.removeMessageListener("SpecialPowers.Quit", spObserver);
-      mm.removeMessageListener("SPPermissionManager", spObserver);
-
-      mm.removeMessageListener("test:SpeechSynthesis:ipcTestMessage", onTestMessage);
-      mm.removeMessageListener("test:SpeechSynthesis:ipcTestComplete", onTestComplete);
-
-      let ppmm = SpecialPowers.Cc["@mozilla.org/parentprocessmessagemanager;1"]
-        .getService(SpecialPowers.Ci.nsIMessageBroadcaster);
-      ppmm.removeMessageListener("test:SpeechSynthesis:ipcSynthAddVoice", onSynthAddVoice);
-      ppmm.removeMessageListener("test:SpeechSynthesis:ipcSynthSetDefault", onSynthSetDefault);
-      ppmm.removeMessageListener("test:SpeechSynthesis:ipcSynthCleanup", onSynthCleanup);
-
-      SimpleTest.executeSoon(function () { SimpleTest.finish(); });
-    }
-
-    function onSynthAddVoice(data) {
-      let message = SpecialPowers.wrap(data).json;
-      return synthAddVoice.apply(synthAddVoice, message);
-    }
-
-    function onSynthSetDefault(data) {
-      let message = SpecialPowers.wrap(data).json;
-      synthSetDefault.apply(synthSetDefault, message);
-    }
-
-    function onSynthCleanup(data) {
-      synthCleanup();
-    }
-
-    function runTests() {
-      let iframe = document.createElement("iframe");
-      SpecialPowers.wrap(iframe).mozbrowser = true;
-      iframe.id = "iframe";
-      iframe.style.width = "100%";
-      iframe.style.height = "1000px";
-
-      function iframeLoadSecond() {
-        ok(true, "Got second iframe load event.");
-        iframe.removeEventListener("mozbrowserloadend", iframeLoadSecond);
-        let mm = SpecialPowers.getBrowserFrameMessageManager(iframe);
-        mm.loadFrameScript("data:,(" + iframeScriptSecond.toString() + ")();",
-                           false);
-      }
-
-      function iframeLoadFirst() {
-        ok(true, "Got first iframe load event.");
-        iframe.removeEventListener("mozbrowserloadend", iframeLoadFirst);
-        iframe.addEventListener("mozbrowserloadend", iframeLoadSecond);
-
-        let mm = SpecialPowers.getBrowserFrameMessageManager(iframe);
-
-        let comp = SpecialPowers.wrap(SpecialPowers.Components);
-
-        let spObserver =
-          comp.classes["@mozilla.org/special-powers-observer;1"]
-              .getService(comp.interfaces.nsIMessageListener);
-
-        mm.addMessageListener("SPPrefService", spObserver);
-        mm.addMessageListener("SPProcessCrashService", spObserver);
-        mm.addMessageListener("SPPingService", spObserver);
-        mm.addMessageListener("SpecialPowers.Quit", spObserver);
-        mm.addMessageListener("SPPermissionManager", spObserver);
-
-        mm.addMessageListener("test:SpeechSynthesis:ipcTestMessage", onTestMessage);
-        mm.addMessageListener("test:SpeechSynthesis:ipcTestComplete", onTestComplete);
-
-        let specialPowersBase = "chrome://specialpowers/content/";
-        mm.loadFrameScript(specialPowersBase + "MozillaLogger.js", false);
-        mm.loadFrameScript(specialPowersBase + "specialpowersAPI.js", false);
-        mm.loadFrameScript(specialPowersBase + "specialpowers.js", false);
-
-        mm.loadFrameScript("data:,(" + iframeScriptFirst.toString() + ")();", false);
-
-        let ppmm = SpecialPowers.Cc["@mozilla.org/parentprocessmessagemanager;1"]
-          .getService(SpecialPowers.Ci.nsIMessageBroadcaster);
-        ppmm.addMessageListener("test:SpeechSynthesis:ipcSynthAddVoice", onSynthAddVoice);
-        ppmm.addMessageListener("test:SpeechSynthesis:ipcSynthSetDefault", onSynthSetDefault);
-        ppmm.addMessageListener("test:SpeechSynthesis:ipcSynthCleanup", onSynthCleanup);
-      }
-
-      iframe.addEventListener("mozbrowserloadend", iframeLoadFirst);
-
-      // Strip this filename and one directory level and then add "/test".
-      let href =  window.location.href;
-      href = href.substring(0, href.lastIndexOf('/'));
-      href = href.substring(0, href.lastIndexOf('/'));
-      href = href.substring(0, href.lastIndexOf('/'));
-      iframe.src = href + "/test?consoleLevel=INFO";
-
-      document.body.appendChild(iframe);
-    }
-
-    addEventListener("load", function() {
-
-      SpecialPowers.addPermission("browser", true, document);
-      SpecialPowers.pushPrefEnv({
-        "set": [
-          // TODO: remove this as part of bug 820712
-          ["network.disable.ipc.security", true],
-
-          ["dom.ipc.browser_frames.oop_by_default", true],
-          ["dom.mozBrowserFramesEnabled", true],
-          ["browser.pagethumbnails.capturing_disabled", true]
-        ]
-      }, runTests);
-    });
-
-  </script>
-</body>
-</html>
deleted file mode 100644
--- a/dom/media/webspeech/synth/ipc/test/mochitest.ini
+++ /dev/null
@@ -1,7 +0,0 @@
-[DEFAULT]
-skip-if = e10s
-support-files =
-  file_ipc.html
-
-[test_ipc.html]
-skip-if = buildapp == 'b2g' || toolkit == 'android' #bug 857673 # b2g(comp.classes['@mozilla.org/special-powers-observer;1'] is undefined) b2g-debug(comp.classes['@mozilla.org/special-powers-observer;1'] is undefined) b2g-desktop(comp.classes['@mozilla.org/special-powers-observer;1'] is undefined)
deleted file mode 100644
--- a/dom/media/webspeech/synth/ipc/test/test_ipc.html
+++ /dev/null
@@ -1,17 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<head>
-  <title>Test for OOP TTS</title>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-</head>
-  <body>
-  <iframe id="testFrame"></iframe>
-  <script type="application/javascript;version=1.7">
-    SimpleTest.waitForExplicitFinish();
-
-    SpecialPowers.pushPrefEnv({ set: [['media.webspeech.synth.enabled', true]] },
-                              function() { document.getElementById("testFrame").src = "file_ipc.html"; });
-  </script>
-</body>
-</html>
--- a/dom/media/webspeech/synth/moz.build
+++ b/dom/media/webspeech/synth/moz.build
@@ -1,16 +1,15 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 if CONFIG['MOZ_WEBSPEECH']:
     MOCHITEST_MANIFESTS += [
-        'ipc/test/mochitest.ini',
         'test/mochitest.ini',
     ]
 
     XPIDL_MODULE = 'dom_webspeechsynth'
 
     XPIDL_SOURCES += [
         'nsISpeechService.idl',
         'nsISynthVoiceRegistry.idl'
@@ -29,16 +28,18 @@ if CONFIG['MOZ_WEBSPEECH']:
     UNIFIED_SOURCES += [
         'ipc/SpeechSynthesisChild.cpp',
         'ipc/SpeechSynthesisParent.cpp',
         'nsSpeechTask.cpp',
         'nsSynthVoiceRegistry.cpp',
         'SpeechSynthesis.cpp',
         'SpeechSynthesisUtterance.cpp',
         'SpeechSynthesisVoice.cpp',
+        'test/FakeSynthModule.cpp',
+        'test/nsFakeSynthServices.cpp'
     ]
 
     if CONFIG['MOZ_SYNTH_PICO']:
         DIRS = ['pico']
 
 IPDL_SOURCES += [
     'ipc/PSpeechSynthesis.ipdl',
     'ipc/PSpeechSynthesisRequest.ipdl',
--- a/dom/media/webspeech/synth/nsSpeechTask.cpp
+++ b/dom/media/webspeech/synth/nsSpeechTask.cpp
@@ -79,17 +79,17 @@ private:
   // and 'mSpeechTask' exclusively owns it and therefor exists as well.
   nsSpeechTask* mSpeechTask;
 
   bool mStarted;
 };
 
 // nsSpeechTask
 
-NS_IMPL_CYCLE_COLLECTION(nsSpeechTask, mSpeechSynthesis, mUtterance);
+NS_IMPL_CYCLE_COLLECTION(nsSpeechTask, mSpeechSynthesis, mUtterance, mCallback);
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsSpeechTask)
   NS_INTERFACE_MAP_ENTRY(nsISpeechTask)
   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsISpeechTask)
 NS_INTERFACE_MAP_END
 
 NS_IMPL_CYCLE_COLLECTING_ADDREF(nsSpeechTask)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(nsSpeechTask)
new file mode 100644
--- /dev/null
+++ b/dom/media/webspeech/synth/test/FakeSynthModule.cpp
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/ModuleUtils.h"
+#include "nsIClassInfoImpl.h"
+
+#include "nsFakeSynthServices.h"
+
+using namespace mozilla::dom;
+
+#define FAKESYNTHSERVICE_CID \
+  {0xe7d52d9e, 0xc148, 0x47d8, {0xab, 0x2a, 0x95, 0xd7, 0xf4, 0x0e, 0xa5, 0x3d}}
+
+#define FAKESYNTHSERVICE_CONTRACTID "@mozilla.org/fakesynth;1"
+
+// Defines nsFakeSynthServicesConstructor
+NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(nsFakeSynthServices,
+                                         nsFakeSynthServices::GetInstanceForService)
+
+// Defines kFAKESYNTHSERVICE_CID
+NS_DEFINE_NAMED_CID(FAKESYNTHSERVICE_CID);
+
+static const mozilla::Module::CIDEntry kCIDs[] = {
+  { &kFAKESYNTHSERVICE_CID, true, nullptr, nsFakeSynthServicesConstructor },
+  { nullptr }
+};
+
+static const mozilla::Module::ContractIDEntry kContracts[] = {
+  { FAKESYNTHSERVICE_CONTRACTID, &kFAKESYNTHSERVICE_CID },
+  { nullptr }
+};
+
+static const mozilla::Module::CategoryEntry kCategories[] = {
+  { "profile-after-change", "Fake Speech Synth", FAKESYNTHSERVICE_CONTRACTID },
+  { nullptr }
+};
+
+static void
+UnloadFakeSynthmodule()
+{
+  nsFakeSynthServices::Shutdown();
+}
+
+static const mozilla::Module kModule = {
+  mozilla::Module::kVersion,
+  kCIDs,
+  kContracts,
+  kCategories,
+  nullptr,
+  nullptr,
+  UnloadFakeSynthmodule
+};
+
+NSMODULE_DEFN(fakesynth) = &kModule;
--- a/dom/media/webspeech/synth/test/common.js
+++ b/dom/media/webspeech/synth/test/common.js
@@ -1,204 +1,8 @@
-var gSpeechRegistry = SpecialPowers.Cc["@mozilla.org/synth-voice-registry;1"]
-  .getService(SpecialPowers.Ci.nsISynthVoiceRegistry);
-
-var gAddedVoices = [];
-
-function SpeechTaskCallback(onpause, onresume, oncancel) {
-  this.onpause = onpause;
-  this.onresume = onresume;
-  this.oncancel = oncancel;
-}
-
-SpeechTaskCallback.prototype = {
-  QueryInterface: function(iid) {
-    return this;
-  },
-
-  getInterfaces: function(c) {},
-
-  getScriptableHelper: function() {},
-
-  onPause: function onPause() {
-    if (this.onpause)
-      this.onpause();
-  },
-
-  onResume: function onResume() {
-    if (this.onresume)
-      this.onresume();
-  },
-
-  onCancel: function onCancel() {
-    if (this.oncancel)
-      this.oncancel();
-  }
-};
-
-var TestSpeechServiceWithAudio = SpecialPowers.wrapCallbackObject({
-  CHANNELS: 1,
-  SAMPLE_RATE: 16000,
-
-  serviceType: SpecialPowers.Ci.nsISpeechService.SERVICETYPE_DIRECT_AUDIO,
-
-  speak: function speak(aText, aUri, aVolume, aRate, aPitch, aTask) {
-    var task = SpecialPowers.wrap(aTask);
-
-    window.setTimeout(
-      function () {
-        task.setup(SpecialPowers.wrapCallbackObject(new SpeechTaskCallback()), this.CHANNELS, this.SAMPLE_RATE);
-        // 0.025 seconds per character.
-        task.sendAudio(new Int16Array((this.SAMPLE_RATE/40)*aText.length), []);
-        task.sendAudio(new Int16Array(0), []);
-      }.bind(this), 0);
-  },
-
-  QueryInterface: function(iid) {
-    return this;
-  },
-
-  getInterfaces: function(c) {},
-
-  getScriptableHelper: function() {}
-});
-
-var TestSpeechServiceNoAudio = SpecialPowers.wrapCallbackObject({
-  serviceType: SpecialPowers.Ci.nsISpeechService.SERVICETYPE_INDIRECT_AUDIO,
-
-  speak: function speak(aText, aUri, aVolume, aRate, aPitch, aTask) {
-    var pair = this.expectedSpeaks.shift();
-    if (pair) {
-      // XXX: These tests do not happen in OOP
-      var utterance = pair[0];
-      var expected = pair[1];
-
-      is(aText, utterance.text, "Speak text matches utterance text");
-
-      var args = {uri: aUri, rate: aRate, pitch: aPitch};
-
-      for (var attr in args) {
-        if (expected[attr] != undefined)
-          is(args[attr], expected[attr], "expected service arg " + attr);
-      }
-    }
-
-    // If the utterance contains the phrase 'callback events', we will dispatch
-    // an appropriate event for each callback method.
-    var no_events = (aText.indexOf('callback events') < 0);
-    // If the utterance contains the phrase 'never end', we don't immediately
-    // end the 'synthesis' of the utterance.
-    var end_utterance = (aText.indexOf('never end') < 0);
-
-    var task = SpecialPowers.wrap(aTask);
-    task.setup(SpecialPowers.wrapCallbackObject(new SpeechTaskCallback(
-      function() {
-        if (!no_events) {
-          task.dispatchPause(1, 1.23);
-        }
-      },
-      function() {
-        if (!no_events) {
-          task.dispatchResume(1, 1.23);
-        }
-      },
-      function() {
-        if (!no_events) {
-          task.dispatchEnd(1, 1.23);
-        }
-      })));
-    setTimeout(function () {
-                 task.dispatchStart();
-                 if (end_utterance) {
-                   setTimeout(function () {
-                                task.dispatchEnd(
-                                  aText.length / 2.0, aText.length);
-                              }, 0);
-                 }
-               }, 0);
-  },
-
-  QueryInterface: function(iid) {
-    return this;
-  },
-
-  getInterfaces: function(c) {},
-
-  getScriptableHelper: function() {},
-
-  expectedSpeaks: []
-});
-
-function synthAddVoice(aServiceName, aName, aLang, aIsLocal) {
-  if (SpecialPowers.isMainProcess()) {
-    var voicesBefore = speechSynthesis.getVoices().length;
-    var uri = "urn:moz-tts:mylittleservice:" + encodeURI(aName + '?' + aLang);
-    gSpeechRegistry.addVoice(window[aServiceName], uri, aName, aLang, aIsLocal);
-
-    gAddedVoices.push([window[aServiceName], uri]);
-    var voicesAfter = speechSynthesis.getVoices().length;
-
-    is(voicesBefore + 1, voicesAfter, "Voice added");
-    var voice = speechSynthesis.getVoices()[voicesAfter - 1];
-    is(voice.voiceURI, uri, "voice URI matches");
-    is(voice.name, aName, "voice name matches");
-    is(voice.lang, aLang, "voice lang matches");
-    is(voice.localService, aIsLocal, "voice localService matches");
-
-    return uri;
-  } else {
-    // XXX: It would be nice to check here that the child gets the voice
-    // added update, but alas, it is aynchronous.
-    var mm = SpecialPowers.Cc["@mozilla.org/childprocessmessagemanager;1"]
-      .getService(SpecialPowers.Ci.nsISyncMessageSender);
-
-    return mm.sendSyncMessage(
-      'test:SpeechSynthesis:ipcSynthAddVoice',
-      [aServiceName, aName, aLang, aIsLocal])[0];
-  }
-}
-
-function synthSetDefault(aUri, aIsDefault) {
-  if (SpecialPowers.isMainProcess()) {
-    gSpeechRegistry.setDefaultVoice(aUri, aIsDefault);
-    var voices = speechSynthesis.getVoices();
-    for (var i in voices) {
-      if (voices[i].voiceURI == aUri)
-        ok(voices[i]['default'], "Voice set to default");
-    }
-  } else {
-    // XXX: It would be nice to check here that the child gets the voice
-    // added update, but alas, it is aynchronous.
-    var mm = SpecialPowers.Cc["@mozilla.org/childprocessmessagemanager;1"]
-      .getService(SpecialPowers.Ci.nsISyncMessageSender);
-
-    return mm.sendSyncMessage(
-      'test:SpeechSynthesis:ipcSynthSetDefault', [aUri, aIsDefault])[0];
-  }
-}
-
-function synthCleanup() {
-  if (SpecialPowers.isMainProcess()) {
-    var voicesBefore = speechSynthesis.getVoices().length;
-    var toRemove = gAddedVoices.length;
-    var removeArgs;
-    while ((removeArgs = gAddedVoices.shift()))
-      gSpeechRegistry.removeVoice.apply(gSpeechRegistry.removeVoice, removeArgs);
-
-    var voicesAfter = speechSynthesis.getVoices().length;
-    is(voicesAfter, voicesBefore - toRemove, "Successfully removed test voices");
-  } else {
-    // XXX: It would be nice to check here that the child gets the voice
-    // removed update, but alas, it is aynchronous.
-    var mm = SpecialPowers.Cc["@mozilla.org/childprocessmessagemanager;1"]
-      .getService(SpecialPowers.Ci.nsISyncMessageSender);
-    mm.sendSyncMessage('test:SpeechSynthesis:ipcSynthCleanup');
-  }
-}
-
 function synthTestQueue(aTestArgs, aEndFunc) {
   var utterances = [];
   for (var i in aTestArgs) {
     var uargs = aTestArgs[i][0];
     var u = new SpeechSynthesisUtterance(uargs.text);
 
     delete uargs.text;
 
@@ -215,24 +19,33 @@ function synthTestQueue(aTestArgs, aEndF
         ok(speechSynthesis.pending, "other utterances queued");
       } else {
         ok(!speechSynthesis.pending, "queue is empty, nothing pending.");
         if (aEndFunc)
           aEndFunc();
       }
     }
 
+    u.addEventListener('start',
+      (function (expectedUri) {
+        return function (e) {
+          if (expectedUri) {
+            var chosenVoice = SpecialPowers.wrap(e).target.chosenVoiceURI;
+            is(chosenVoice, expectedUri, "Incorrect URI is used");
+          }
+        };
+      })(aTestArgs[i][1] ? aTestArgs[i][1].uri : null));
+
     u.addEventListener('end', onend_handler);
     u.addEventListener('error', onend_handler);
 
     u.addEventListener(
       'error', function onerror_handler(e) {
         ok(false, "Error in speech utterance '" + e.target.text + "'");
       });
 
     utterances.push(u);
-    TestSpeechServiceNoAudio.expectedSpeaks.push([u, aTestArgs[i][1]]);
     speechSynthesis.speak(u);
   }
 
   ok(!speechSynthesis.speaking, "speechSynthesis is not speaking yet.");
   ok(speechSynthesis.pending, "speechSynthesis has an utterance queued.");
 }
--- a/dom/media/webspeech/synth/test/file_indirect_service_events.html
+++ b/dom/media/webspeech/synth/test/file_indirect_service_events.html
@@ -21,56 +21,54 @@ https://bugzilla.mozilla.org/show_bug.cg
 <div id="content" style="display: none">
 
 </div>
 <pre id="test">
 <script type="application/javascript">
 
 /** Test for Bug 1155034 **/
 
-synthAddVoice('TestSpeechServiceNoAudio', 'Female 1', 'en-GB', true);
-
 function test_with_events() {
   info('test_with_events');
   var utterance = new SpeechSynthesisUtterance("never end, callback events");
+  utterance.lang = 'it-IT-noend';
 
   utterance.addEventListener('start', function(e) {
     speechSynthesis.pause();
   // Wait to see if we get some bad events we didn't expect.
   });
 
   utterance.addEventListener('pause', function(e) {
-    ok(e.charIndex, 1, 'pause event charIndex matches service arguments');
-    ok(e.elapsedTime, 1.23, 'pause event elapsedTime matches service arguments');
+    is(e.charIndex, 1, 'pause event charIndex matches service arguments');
+    is(e.elapsedTime, 1.5, 'pause event elapsedTime matches service arguments');
     speechSynthesis.resume();
   });
 
   utterance.addEventListener('resume', function(e) {
-    ok(e.charIndex, 1, 'resume event charIndex matches service arguments');
-    ok(e.elapsedTime, 1.23, 'resume event elapsedTime matches service arguments');
+    is(e.charIndex, 1, 'resume event charIndex matches service arguments');
+    is(e.elapsedTime, 1.5, 'resume event elapsedTime matches service arguments');
     speechSynthesis.cancel();
   });
 
   utterance.addEventListener('end', function(e) {
     ok(e.charIndex, 1, 'resume event charIndex matches service arguments');
-    ok(e.elapsedTime, 1.23, 'end event elapsedTime matches service arguments');
+    ok(e.elapsedTime, 1.5, 'end event elapsedTime matches service arguments');
     test_no_events();
   });
 
   speechSynthesis.speak(utterance);
 }
 
 function test_no_events() {
   var utterance = new SpeechSynthesisUtterance("never end");
-
+  utterance.lang = "it-IT-noevents-noend";
   utterance.addEventListener('start', function(e) {
     speechSynthesis.pause();
     // Wait to see if we get some bad events we didn't expect.
     setTimeout(function() {
-      synthCleanup();
       SimpleTest.finish();
     }, 1000);
   });
 
   utterance.addEventListener('pause', function(e) {
     ok(false, 'no pause event was explicitly dispatched from the service')
     speechSynthesis.resume();
   });
--- a/dom/media/webspeech/synth/test/file_setup.html
+++ b/dom/media/webspeech/synth/test/file_setup.html
@@ -20,22 +20,16 @@ https://bugzilla.mozilla.org/show_bug.cg
 <div id="content" style="display: none">
   
 </div>
 <pre id="test">
 <script type="application/javascript">
 
 /** Test for Bug 525444 **/
 
-synthAddVoice('TestSpeechServiceNoAudio', 'Bob Marley', 'en-JM', true);
-synthAddVoice('TestSpeechServiceNoAudio', 'Amy Winehouse', 'en-GB', true);
-synthAddVoice('TestSpeechServiceNoAudio', 'Leonard Cohen', 'en-CA', true);
-synthAddVoice('TestSpeechServiceNoAudio', 'Celine Dion', 'fr-CA', true);
-synthAddVoice('TestSpeechServiceNoAudio', 'Julieta Venegas', 'es-MX', true);
-
 ok(SpeechSynthesis, "SpeechSynthesis exists in global scope");
 ok(SpeechSynthesisVoice, "SpeechSynthesisVoice exists in global scope");
 ok(SpeechSynthesisEvent, "SpeechSynthesisEvent exists in global scope");
 
 // SpeechSynthesisUtterance is the only type that has a constructor
 //  and writable properties
 ok(SpeechSynthesisUtterance, "SpeechSynthesisUtterance exists in global scope");
 var ssu = new SpeechSynthesisUtterance("hello world");
@@ -70,15 +64,13 @@ var voices1 = speechSynthesis.getVoices(
 var voices2 = speechSynthesis.getVoices();
 
 ok(voices1.length == voices2.length, "Voice count matches");
 
 for (var i in voices1) {
   ok(voices1[i] == voices2[i], "Voice instance matches");
 }
 
-synthCleanup();
-
 SimpleTest.finish();
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/webspeech/synth/test/file_speech_cancel.html
+++ b/dom/media/webspeech/synth/test/file_speech_cancel.html
@@ -21,40 +21,58 @@ https://bugzilla.mozilla.org/show_bug.cg
 <div id="content" style="display: none">
 
 </div>
 <pre id="test">
 <script type="application/javascript">
 
 /** Test for Bug 1150315 **/
 
-synthAddVoice('TestSpeechServiceWithAudio', 'Male 1', 'en-GB', true);
-
 var gotEndEvent = false;
-var utterance = new SpeechSynthesisUtterance("Hello, world!");
+// A long utterance that we will interrupt.
+var utterance = new SpeechSynthesisUtterance("Donec ac nunc feugiat, posuere " +
+  "mauris id, pharetra velit. Donec fermentum orci nunc, sit amet maximus" +
+  "dui tincidunt ut. Sed ultricies ac nisi a laoreet. Proin interdum," +
+  "libero maximus hendrerit posuere, lorem risus egestas nisl, a" +
+  "ultricies massa justo eu nisi. Duis mattis nibh a ligula tincidunt" +
+  "tincidunt non eu erat. Sed bibendum varius vulputate. Cras leo magna," +
+  "ornare ac posuere vel, luctus id metus. Mauris nec quam ac augue" +
+  "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();
   speechSynthesis.speak(utterance2);
 });
 
-var utterance2 = new SpeechSynthesisUtterance("Hello, world 2!");
+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" +
+  "diam. Nulla sed odio elit.");
 utterance2.addEventListener('start', function() {
   speechSynthesis.cancel();
   speechSynthesis.speak(utterance3);
 });
 utterance2.addEventListener('end', function(e) {
   gotEndEvent = true;
 });
 
 var utterance3 = new SpeechSynthesisUtterance("Hello, world 3!");
 utterance3.addEventListener('start', function() {
-  ok(gotEndEvent, "didn't get start event for this utterance")
+  ok(gotEndEvent, "didn't get start event for this utterance");
 });
 utterance3.addEventListener('end', function(e) {
-  synthCleanup();
   SimpleTest.finish();
 });
 
 speechSynthesis.speak(utterance);
 ok(!speechSynthesis.speaking, "speechSynthesis is not speaking yet.");
 ok(speechSynthesis.pending, "speechSynthesis has an utterance queued.");
 
 </script>
--- a/dom/media/webspeech/synth/test/file_speech_queue.html
+++ b/dom/media/webspeech/synth/test/file_speech_queue.html
@@ -13,58 +13,64 @@ https://bugzilla.mozilla.org/show_bug.cg
     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=525444">Mozilla Bug 525444</a>
 <p id="display"></p>
 <div id="content" style="display: none">
-  
+
 </div>
 <pre id="test">
 <script type="application/javascript">
 
 /** Test for Bug 525444 **/
 
-var englishJamaican = synthAddVoice('TestSpeechServiceNoAudio',
-                                    'Bob Marley', 'en-JM', true);
-var englishBritish = synthAddVoice('TestSpeechServiceNoAudio',
-                                   'Amy Winehouse', 'en-GB', true);
-var englishCanadian = synthAddVoice('TestSpeechServiceNoAudio',
-                                    'Leonard Cohen', 'en-CA', true);
-var frenchCanadian = synthAddVoice('TestSpeechServiceNoAudio',
-                                   'Celine Dion', 'fr-CA', true);
-var spanishMexican = synthAddVoice('TestSpeechServiceNoAudio',
-                                   'Julieta Venegas', 'es-MX', true);
+// XXX: Rate and pitch are not tested.
+
+var langUriMap = {};
 
-synthSetDefault(englishBritish, true);
+for (var voice of speechSynthesis.getVoices()) {
+  if (voice.voiceURI.indexOf('urn:moz-tts:fake-direct') < 0) {
+    continue;
+  }
+  langUriMap[voice.lang] = voice.voiceURI;
+  ok(true, voice.lang + ' ' + voice.voiceURI + ' ' + voice.default);
+  is(voice.default, voice.lang == 'en-JM', 'Only Jamaican voice should be default');
+}
+
+ok(langUriMap['en-JM'], 'No English-Jamaican voice');
+ok(langUriMap['en-GB'], 'No English-British voice');
+ok(langUriMap['en-CA'], 'No English-Canadian voice');
+ok(langUriMap['fr-CA'], 'No French-Canadian voice');
+ok(langUriMap['es-MX'], 'No Spanish-Mexican voice');
 
 synthTestQueue(
   [[{text: "Hello, world."},
-    { uri: englishBritish }],
+    { uri: langUriMap['en-JM'] }],
    [{text: "Bonjour tout le monde .", lang: "fr", rate: 0.5, pitch: 0.75},
-    { uri: frenchCanadian, rate: 0.5, pitch: 0.75}],
+    { uri: langUriMap['fr-CA'], rate: 0.5, pitch: 0.75}],
    [{text: "How are you doing?", lang: "en-GB"},
-    { rate: 1, pitch: 1, uri: englishBritish}],
-   [{text: "¡hasta mañana", lang: "es-ES"},
-    { uri: spanishMexican }]],
+    { rate: 1, pitch: 1, uri: langUriMap['en-GB']}],
+   [{text: "¡hasta mañana!", lang: "es-MX"},
+    { uri: langUriMap['es-MX'] }]],
   function () {
-    synthSetDefault(englishJamaican, true);
-    var test_data = [[{text: "I shot the  sheriff."},
-                      { uri: englishJamaican }]];
+    var test_data = [];
     var voices = speechSynthesis.getVoices();
-    for (var i in voices) {
-      test_data.push([{text: "Hello world", voice: voices[i]},
-                      {uri: voices[i].voiceURI}]);
+    for (var voice of voices) {
+      if (voice.voiceURI.indexOf('urn:moz-tts:fake-direct') < 0) {
+        continue;
+      }
+      test_data.push([{text: "Hello world", voice: voice},
+                      {uri: voice.voiceURI}]);
     }
 
     synthTestQueue(test_data,
                    function () {
-                     synthCleanup();
                      SimpleTest.finish();
                    });
   });
 
 
 
 </script>
 </pre>
--- a/dom/media/webspeech/synth/test/file_speech_simple.html
+++ b/dom/media/webspeech/synth/test/file_speech_simple.html
@@ -21,33 +21,30 @@ https://bugzilla.mozilla.org/show_bug.cg
 <div id="content" style="display: none">
   
 </div>
 <pre id="test">
 <script type="application/javascript">
 
 /** Test for Bug 525444 **/
 
-synthAddVoice('TestSpeechServiceWithAudio', 'Male 1', 'en-GB', true);
-
 var gotStartEvent = false;
 var gotBoundaryEvent = false;
 var utterance = new SpeechSynthesisUtterance("Hello, world!");
 utterance.addEventListener('start', function(e) {
   ok(speechSynthesis.speaking, "speechSynthesis is speaking.");
   ok(!speechSynthesis.pending, "speechSynthesis has no other utterances queued.");
   gotStartEvent = true;
 });
 
 utterance.addEventListener('end', function(e) {
   ok(!speechSynthesis.speaking, "speechSynthesis is not speaking.");
   ok(!speechSynthesis.pending, "speechSynthesis has no other utterances queued.");
   ok(gotStartEvent, "Got 'start' event.");
   info('end ' + e.elapsedTime);
-  synthCleanup();
   SimpleTest.finish();
 });
 
 speechSynthesis.speak(utterance);
 ok(!speechSynthesis.speaking, "speechSynthesis is not speaking yet.");
 ok(speechSynthesis.pending, "speechSynthesis has an utterance queued.");
 
 </script>
--- a/dom/media/webspeech/synth/test/mochitest.ini
+++ b/dom/media/webspeech/synth/test/mochitest.ini
@@ -1,19 +1,14 @@
 [DEFAULT]
-skip-if = e10s
 support-files =
   common.js
   file_setup.html
   file_speech_queue.html
   file_speech_simple.html
   file_speech_cancel.html
   file_indirect_service_events.html
 
 [test_setup.html]
 [test_speech_queue.html]
-skip-if = buildapp == 'b2g' # b2g(Test timed out)
 [test_speech_simple.html]
-skip-if = buildapp == 'b2g' # b2g(Test timed out)
 [test_speech_cancel.html]
-skip-if = toolkit == 'gonk' # b2g(Test timed out)
 [test_indirect_service_events.html]
-skip-if = toolkit == 'gonk' # b2g(Test timed out)
new file mode 100644
--- /dev/null
+++ b/dom/media/webspeech/synth/test/nsFakeSynthServices.cpp
@@ -0,0 +1,359 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.h"
+#include "nsFakeSynthServices.h"
+#include "nsPrintfCString.h"
+#include "nsIWeakReferenceUtils.h"
+#include "SharedBuffer.h"
+#include "nsISimpleEnumerator.h"
+
+#include "mozilla/dom/nsSynthVoiceRegistry.h"
+#include "mozilla/dom/nsSpeechTask.h"
+
+#include "nsThreadUtils.h"
+#include "prenv.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/DebugOnly.h"
+
+#define CHANNELS 1
+#define SAMPLERATE 1600
+
+namespace mozilla {
+namespace dom {
+
+StaticRefPtr<nsFakeSynthServices> nsFakeSynthServices::sSingleton;
+
+enum VoiceFlags
+{
+  eSuppressEvents = 1,
+  eSuppressEnd = 2
+};
+
+struct VoiceDetails
+{
+  const char* uri;
+  const char* name;
+  const char* lang;
+  bool defaultVoice;
+  uint32_t flags;
+};
+
+static const VoiceDetails sDirectVoices[] = {
+  {"urn:moz-tts:fake-direct:bob", "Bob Marley", "en-JM", true, 0},
+  {"urn:moz-tts:fake-direct:amy", "Amy Winehouse", "en-GB", false, 0},
+  {"urn:moz-tts:fake-direct:lenny", "Leonard Cohen", "en-CA", false, 0},
+  {"urn:moz-tts:fake-direct:celine", "Celine Dion", "fr-CA", false, 0},
+  {"urn:moz-tts:fake-direct:julie", "Julieta Venegas", "es-MX", false, },
+};
+
+static const VoiceDetails sIndirectVoices[] = {
+  {"urn:moz-tts:fake-indirect:zanetta", "Zanetta Farussi", "it-IT", false, 0},
+  {"urn:moz-tts:fake-indirect:margherita", "Margherita Durastanti", "it-IT-noevents-noend", false, eSuppressEvents | eSuppressEnd},
+  {"urn:moz-tts:fake-indirect:teresa", "Teresa Cornelys", "it-IT-noend", false, eSuppressEnd},
+};
+
+// FakeSynthCallback
+class FakeSynthCallback : public nsISpeechTaskCallback
+{
+public:
+  explicit FakeSynthCallback(nsISpeechTask* aTask) : mTask(aTask) { }
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(FakeSynthCallback, nsISpeechTaskCallback)
+
+  NS_IMETHOD OnPause()
+  {
+    if (mTask) {
+      mTask->DispatchPause(1.5, 1);
+    }
+
+    return NS_OK;
+  }
+
+  NS_IMETHOD OnResume()
+  {
+    if (mTask) {
+      mTask->DispatchResume(1.5, 1);
+    }
+
+    return NS_OK;
+  }
+
+  NS_IMETHOD OnCancel()
+  {
+    if (mTask) {
+      mTask->DispatchEnd(1.5, 1);
+    }
+
+    return NS_OK;
+  }
+
+private:
+  virtual ~FakeSynthCallback() { }
+
+  nsCOMPtr<nsISpeechTask> mTask;
+};
+
+NS_IMPL_CYCLE_COLLECTION(FakeSynthCallback, mTask);
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FakeSynthCallback)
+  NS_INTERFACE_MAP_ENTRY(nsISpeechTaskCallback)
+  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsISpeechTaskCallback)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(FakeSynthCallback)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(FakeSynthCallback)
+
+// FakeDirectAudioSynth
+
+class FakeDirectAudioSynth : public nsISpeechService
+{
+
+public:
+  FakeDirectAudioSynth() { }
+
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSISPEECHSERVICE
+
+private:
+  virtual ~FakeDirectAudioSynth() { }
+};
+
+NS_IMPL_ISUPPORTS(FakeDirectAudioSynth, nsISpeechService)
+
+NS_IMETHODIMP
+FakeDirectAudioSynth::Speak(const nsAString& aText, const nsAString& aUri,
+                            float aVolume, float aRate, float aPitch,
+                            nsISpeechTask* aTask)
+{
+  class Runnable final : public nsRunnable
+  {
+  public:
+    Runnable(nsISpeechTask* aTask, const nsAString& aText) :
+      mTask(aTask), mText(aText)
+    {
+    }
+
+    NS_IMETHOD Run() override
+    {
+      nsRefPtr<FakeSynthCallback> cb = new FakeSynthCallback(nullptr);
+      mTask->Setup(cb, CHANNELS, SAMPLERATE, 2);
+
+      // Just an arbitrary multiplier. Pretend that each character is
+      // synthesized to 40 frames.
+      uint32_t frames_length = 40 * mText.Length();
+      nsAutoArrayPtr<int16_t> frames(new int16_t[frames_length]());
+      mTask->SendAudioNative(frames, frames_length);
+
+      mTask->SendAudioNative(nullptr, 0);
+
+      return NS_OK;
+    }
+
+  private:
+    nsCOMPtr<nsISpeechTask> mTask;
+    nsString mText;
+  };
+
+  nsCOMPtr<nsIRunnable> runnable = new Runnable(aTask, aText);
+  NS_DispatchToMainThread(runnable);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+FakeDirectAudioSynth::GetServiceType(SpeechServiceType* aServiceType)
+{
+  *aServiceType = nsISpeechService::SERVICETYPE_DIRECT_AUDIO;
+  return NS_OK;
+}
+
+// FakeDirectAudioSynth
+
+class FakeIndirectAudioSynth : public nsISpeechService
+{
+
+public:
+  FakeIndirectAudioSynth() {}
+
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSISPEECHSERVICE
+
+private:
+  virtual ~FakeIndirectAudioSynth() { }
+};
+
+NS_IMPL_ISUPPORTS(FakeIndirectAudioSynth, nsISpeechService)
+
+NS_IMETHODIMP
+FakeIndirectAudioSynth::Speak(const nsAString& aText, const nsAString& aUri,
+                              float aVolume, float aRate, float aPitch,
+                              nsISpeechTask* aTask)
+{
+  class DispatchStart final : public nsRunnable
+  {
+  public:
+    explicit DispatchStart(nsISpeechTask* aTask) :
+      mTask(aTask)
+    {
+    }
+
+    NS_IMETHOD Run() override
+    {
+      mTask->DispatchStart();
+
+      return NS_OK;
+    }
+
+  private:
+    nsCOMPtr<nsISpeechTask> mTask;
+  };
+
+  class DispatchEnd final : public nsRunnable
+  {
+  public:
+    DispatchEnd(nsISpeechTask* aTask, const nsAString& aText) :
+      mTask(aTask), mText(aText)
+    {
+    }
+
+    NS_IMETHOD Run() override
+    {
+      mTask->DispatchEnd(mText.Length()/2, mText.Length());
+
+      return NS_OK;
+    }
+
+  private:
+    nsCOMPtr<nsISpeechTask> mTask;
+    nsString mText;
+  };
+
+  uint32_t flags = 0;
+  for (uint32_t i = 0; i < ArrayLength(sIndirectVoices); i++) {
+    if (aUri.EqualsASCII(sIndirectVoices[i].uri)) {
+      flags = sIndirectVoices[i].flags;
+    }
+  }
+
+  nsRefPtr<FakeSynthCallback> cb = new FakeSynthCallback(
+    (flags & eSuppressEvents) ? nullptr : aTask);
+
+  aTask->Setup(cb, 0, 0, 0);
+
+  nsCOMPtr<nsIRunnable> runnable = new DispatchStart(aTask);
+  NS_DispatchToMainThread(runnable);
+
+  if ((flags & eSuppressEnd) == 0) {
+    runnable = new DispatchEnd(aTask, aText);
+    NS_DispatchToMainThread(runnable);
+  }
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+FakeIndirectAudioSynth::GetServiceType(SpeechServiceType* aServiceType)
+{
+  *aServiceType = nsISpeechService::SERVICETYPE_INDIRECT_AUDIO;
+  return NS_OK;
+}
+
+// nsFakeSynthService
+
+NS_INTERFACE_MAP_BEGIN(nsFakeSynthServices)
+  NS_INTERFACE_MAP_ENTRY(nsIObserver)
+  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_ADDREF(nsFakeSynthServices)
+NS_IMPL_RELEASE(nsFakeSynthServices)
+
+nsFakeSynthServices::nsFakeSynthServices()
+{
+}
+
+nsFakeSynthServices::~nsFakeSynthServices()
+{
+}
+
+static void
+AddVoices(nsISpeechService* aService, const VoiceDetails* aVoices, uint32_t aLength)
+{
+  nsSynthVoiceRegistry* registry = nsSynthVoiceRegistry::GetInstance();
+  for (uint32_t i = 0; i < aLength; i++) {
+    NS_ConvertUTF8toUTF16 name(aVoices[i].name);
+    NS_ConvertUTF8toUTF16 uri(aVoices[i].uri);
+    NS_ConvertUTF8toUTF16 lang(aVoices[i].lang);
+    registry->AddVoice(aService, uri, name, lang, true);
+    if (aVoices[i].defaultVoice) {
+      registry->SetDefaultVoice(uri, true);
+    }
+  }
+}
+
+void
+nsFakeSynthServices::Init()
+{
+  mDirectService = new FakeDirectAudioSynth();
+  AddVoices(mDirectService, sDirectVoices, ArrayLength(sDirectVoices));
+
+  mIndirectService = new FakeIndirectAudioSynth();
+  AddVoices(mIndirectService, sIndirectVoices, ArrayLength(sIndirectVoices));
+}
+
+// nsIObserver
+
+NS_IMETHODIMP
+nsFakeSynthServices::Observe(nsISupports* aSubject, const char* aTopic,
+                             const char16_t* aData)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  NS_ENSURE_TRUE(!strcmp(aTopic, "profile-after-change"), NS_ERROR_UNEXPECTED);
+
+  if (Preferences::GetBool("media.webspeech.synth.test")) {
+    Init();
+  }
+
+  return NS_OK;
+}
+
+// static methods
+
+nsFakeSynthServices*
+nsFakeSynthServices::GetInstance()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  if (XRE_GetProcessType() != GeckoProcessType_Default) {
+    MOZ_ASSERT(false, "nsFakeSynthServices can only be started on main gecko process");
+    return nullptr;
+  }
+
+  if (!sSingleton) {
+    sSingleton = new nsFakeSynthServices();
+  }
+
+  return sSingleton;
+}
+
+already_AddRefed<nsFakeSynthServices>
+nsFakeSynthServices::GetInstanceForService()
+{
+  nsRefPtr<nsFakeSynthServices> picoService = GetInstance();
+  return picoService.forget();
+}
+
+void
+nsFakeSynthServices::Shutdown()
+{
+  if (!sSingleton) {
+    return;
+  }
+
+  sSingleton = nullptr;
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/media/webspeech/synth/test/nsFakeSynthServices.h
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsFakeSynthServices_h
+#define nsFakeSynthServices_h
+
+#include "nsAutoPtr.h"
+#include "nsTArray.h"
+#include "nsIObserver.h"
+#include "nsIThread.h"
+#include "nsISpeechService.h"
+#include "nsRefPtrHashtable.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/Monitor.h"
+
+namespace mozilla {
+namespace dom {
+
+class nsFakeSynthServices : public nsIObserver
+{
+
+public:
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSIOBSERVER
+
+  nsFakeSynthServices();
+
+  static nsFakeSynthServices* GetInstance();
+
+  static already_AddRefed<nsFakeSynthServices> GetInstanceForService();
+
+  static void Shutdown();
+
+private:
+
+  virtual ~nsFakeSynthServices();
+
+  void Init();
+
+  nsCOMPtr<nsISpeechService> mDirectService;
+
+  nsCOMPtr<nsISpeechService> mIndirectService;
+
+  static StaticRefPtr<nsFakeSynthServices> sSingleton;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif