Merge inbound to m-c a=merge
authorWes Kocher <wkocher@mozilla.com>
Thu, 29 Jan 2015 15:01:38 -0800
changeset 226601 4380ed39de3aa0a0b5c5ac06a5dd7e13d226fa74
parent 226538 49a3acc91bd3558d03a32e6bcbee754a17c498f6 (current diff)
parent 226600 b556a1f684ed50dfc6f649e22ab4dd7d75aa1c62 (diff)
child 226618 0de614a1d5f1439f22f9e5c62e66e5484979ca5d
push id28200
push userkwierso@gmail.com
push dateThu, 29 Jan 2015 23:01:46 +0000
treeherdermozilla-central@4380ed39de3a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone38.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
Merge inbound to m-c a=merge
js/src/jscrashformat.h
js/src/jscrashreport.cpp
js/src/jscrashreport.h
layout/inspector/inFlasher.cpp
layout/inspector/inFlasher.h
layout/inspector/inIFlasher.idl
--- a/CLOBBER
+++ b/CLOBBER
@@ -17,9 +17,9 @@
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
 
 # Are you updating CLOBBER because you think it's needed for your WebIDL
 # changes to stick? As of bug 928195, this shouldn't be necessary! Please
 # don't change CLOBBER for WebIDL changes any more.
 
-Bug 1123384 - Needs a clobber for some reason.
+Bug 1018324 - Clobber should fix burning tree
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -3755,23 +3755,24 @@ function FillHistoryMenu(aParent) {
   var tooltipBack = gNavigatorBundle.getString("tabHistory.goBack");
   var tooltipCurrent = gNavigatorBundle.getString("tabHistory.current");
   var tooltipForward = gNavigatorBundle.getString("tabHistory.goForward");
 
   for (var j = end - 1; j >= start; j--) {
     let item = document.createElement("menuitem");
     let entry = sessionHistory.getEntryAtIndex(j, false);
     let uri = entry.URI.spec;
+    let uriCopy = BrowserUtils.makeURI(uri);
 
     item.setAttribute("uri", uri);
     item.setAttribute("label", entry.title || uri);
     item.setAttribute("index", j);
 
     if (j != index) {
-      PlacesUtils.favicons.getFaviconURLForPage(entry.URI, function (aURI) {
+      PlacesUtils.favicons.getFaviconURLForPage(uriCopy, function (aURI) {
         if (aURI) {
           let iconURL = PlacesUtils.favicons.getFaviconLinkForIcon(aURI).spec;
           iconURL = PlacesUtils.getImageURLForResolution(window, iconURL);
           item.style.listStyleImage = "url(" + iconURL + ")";
         }
       });
     }
 
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -157,17 +157,20 @@ let handleContentContextMenu = function 
     let spellInfo;
     if (editFlags &
         (SpellCheckHelper.EDITABLE | SpellCheckHelper.CONTENTEDITABLE)) {
       spellInfo =
         InlineSpellCheckerContent.initContextMenu(event, editFlags, this);
     }
 
     let customMenuItems = PageMenuChild.build(event.target);
-    sendSyncMessage("contextmenu", { editFlags, spellInfo, customMenuItems, addonInfo }, { event, popupNode: event.target });
+    let principal = event.target.ownerDocument.nodePrincipal;
+    sendSyncMessage("contextmenu",
+                    { editFlags, spellInfo, customMenuItems, addonInfo, principal },
+                    { event, popupNode: event.target });
   }
   else {
     // Break out to the parent window and pass the add-on info along
     let browser = docShell.chromeEventHandler;
     let mainWin = browser.ownerDocument.defaultView;
     mainWin.gContextMenuContentData = {
       isRemote: false,
       event: event,
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -591,23 +591,25 @@ nsContextMenu.prototype = {
     let [elt, win] = BrowserUtils.getFocusSync(document);
     this.focusedWindow = win;
     this.focusedElement = elt;
 
     // If this is a remote context menu event, use the information from
     // gContextMenuContentData instead.
     if (this.isRemote) {
       this.browser = gContextMenuContentData.browser;
+      this.principal = gContextMenuContentData.principal;
     } else {
       editFlags = SpellCheckHelper.isEditable(this.target, window);
       this.browser = this.target.ownerDocument.defaultView
                                   .QueryInterface(Ci.nsIInterfaceRequestor)
                                   .getInterface(Ci.nsIWebNavigation)
                                   .QueryInterface(Ci.nsIDocShell)
                                   .chromeEventHandler;
+      this.principal = this.target.ownerDocument.nodePrincipal;
     }
     this.onSocial = !!this.browser.getAttribute("origin");
 
     // Check if we are in a synthetic document (stand alone image, video, etc.).
     this.inSyntheticDoc =  this.target.ownerDocument.mozSyntheticDocument;
     // First, do checks for nodes that never have children.
     if (this.target.nodeType == Node.ELEMENT_NODE) {
       // See if the user clicked on an image.
@@ -829,28 +831,16 @@ nsContextMenu.prototype = {
     // until we do.
     return this.linkProtocol && !(
              this.linkProtocol == "mailto"     ||
              this.linkProtocol == "javascript" ||
              this.linkProtocol == "news"       ||
              this.linkProtocol == "snews"      );
   },
 
-  _unremotePrincipal: function(aRemotePrincipal) {
-    if (this.isRemote) {
-      return Cc["@mozilla.org/scriptsecuritymanager;1"]
-               .getService(Ci.nsIScriptSecurityManager)
-               .getAppCodebasePrincipal(aRemotePrincipal.URI,
-                                        aRemotePrincipal.appId,
-                                        aRemotePrincipal.isInBrowserElement);
-    }
-
-    return aRemotePrincipal;
-  },
-
   _isSpellCheckEnabled: function(aNode) {
     // We can always force-enable spellchecking on textboxes
     if (this.isTargetATextBox(aNode)) {
       return true;
     }
     // We can never spell check something which is not content editable
     var editable = aNode.isContentEditable;
     if (!editable && aNode.ownerDocument) {
@@ -870,32 +860,32 @@ nsContextMenu.prototype = {
     for (let p in extra)
       params[p] = extra[p];
     return params;
   },
 
   // Open linked-to URL in a new window.
   openLink : function () {
     var doc = this.target.ownerDocument;
-    urlSecurityCheck(this.linkURL, this._unremotePrincipal(doc.nodePrincipal));
+    urlSecurityCheck(this.linkURL, this.principal);
     openLinkIn(this.linkURL, "window", this._openLinkInParameters(doc));
   },
 
   // Open linked-to URL in a new private window.
   openLinkInPrivateWindow : function () {
     var doc = this.target.ownerDocument;
-    urlSecurityCheck(this.linkURL, this._unremotePrincipal(doc.nodePrincipal));
+    urlSecurityCheck(this.linkURL, this.principal);
     openLinkIn(this.linkURL, "window",
                this._openLinkInParameters(doc, { private: true }));
   },
 
   // Open linked-to URL in a new tab.
   openLinkInTab: function() {
     var doc = this.target.ownerDocument;
-    urlSecurityCheck(this.linkURL, this._unremotePrincipal(doc.nodePrincipal));
+    urlSecurityCheck(this.linkURL, this.principal);
     var referrerURI = doc.documentURIObject;
 
     // if the mixedContentChannel is present and the referring URI passes
     // a same origin check with the target URI, we can preserve the users
     // decision of disabling MCB on a page for it's child tabs.
     var persistAllowMixedContentInChildTab = false;
 
     if (this.browser.docShell && this.browser.docShell.mixedContentChannel) {
@@ -912,17 +902,17 @@ nsContextMenu.prototype = {
       allowMixedContent: persistAllowMixedContentInChildTab,
     });
     openLinkIn(this.linkURL, "tab", params);
   },
 
   // open URL in current tab
   openLinkInCurrent: function() {
     var doc = this.target.ownerDocument;
-    urlSecurityCheck(this.linkURL, this._unremotePrincipal(doc.nodePrincipal));
+    urlSecurityCheck(this.linkURL, this.principal);
     openLinkIn(this.linkURL, "current", this._openLinkInParameters(doc));
   },
 
   // Open frame in a new tab.
   openFrameInTab: function() {
     var doc = this.target.ownerDocument;
     var frameURL = doc.location.href;
     var referrer = doc.referrer;
@@ -1120,18 +1110,17 @@ nsContextMenu.prototype = {
 
   setDesktopBackground: function() {
     // Paranoia: check disableSetDesktopBackground again, in case the
     // image changed since the context menu was initiated.
     if (this.disableSetDesktopBackground())
       return;
 
     var doc = this.target.ownerDocument;
-    urlSecurityCheck(this.target.currentURI.spec,
-                     this._unremotePrincipal(doc.nodePrincipal));
+    urlSecurityCheck(this.target.currentURI.spec, this.principal);
 
     // Confirm since it's annoying if you hit this accidentally.
     const kDesktopBackgroundURL = 
                   "chrome://browser/content/setDesktopBackground.xul";
 #ifdef XP_MACOSX
     // On Mac, the Set Desktop Background window is not modal.
     // Don't open more than one Set Desktop Background window.
     var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
@@ -1298,17 +1287,17 @@ nsContextMenu.prototype = {
   saveLink: function() {
     var doc =  this.target.ownerDocument;
     var linkText;
     // If selected text is found to match valid URL pattern.
     if (this.onPlainTextLink)
       linkText = this.focusedWindow.getSelection().toString().trim();
     else
       linkText = this.linkText();
-    urlSecurityCheck(this.linkURL, this._unremotePrincipal(doc.nodePrincipal));
+    urlSecurityCheck(this.linkURL, this.principal);
 
     this.saveHelper(this.linkURL, linkText, null, true, doc);
   },
 
   // Backwards-compatibility wrapper
   saveImage : function() {
     if (this.onCanvas || this.onImage)
         this.saveMedia();
@@ -1318,24 +1307,22 @@ nsContextMenu.prototype = {
   saveMedia: function() {
     var doc =  this.target.ownerDocument;
     if (this.onCanvas) {
       // Bypass cache, since it's a data: URL.
       saveImageURL(this.target.toDataURL(), "canvas.png", "SaveImageTitle",
                    true, false, doc.documentURIObject, doc);
     }
     else if (this.onImage) {
-      urlSecurityCheck(this.mediaURL,
-                       this._unremotePrincipal(doc.nodePrincipal));
+      urlSecurityCheck(this.mediaURL, this.principal);
       saveImageURL(this.mediaURL, null, "SaveImageTitle", false,
                    false, doc.documentURIObject, doc);
     }
     else if (this.onVideo || this.onAudio) {
-      urlSecurityCheck(this.mediaURL,
-                       this._unremotePrincipal(doc.nodePrincipal));
+      urlSecurityCheck(this.mediaURL, this.principal);
       var dialogTitle = this.onVideo ? "SaveVideoTitle" : "SaveAudioTitle";
       this.saveHelper(this.mediaURL, null, dialogTitle, false, doc);
     }
   },
 
   // Backwards-compatibility wrapper
   sendImage : function() {
     if (this.onCanvas || this.onImage)
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -3166,16 +3166,17 @@
               if (spellInfo)
                 spellInfo.target = aMessage.target.messageManager;
               gContextMenuContentData = { isRemote: true,
                                           event: aMessage.objects.event,
                                           popupNode: aMessage.objects.popupNode,
                                           browser: browser,
                                           editFlags: aMessage.data.editFlags,
                                           spellInfo: spellInfo,
+                                          principal: aMessage.data.principal,
                                           customMenuItems: aMessage.data.customMenuItems,
                                           addonInfo: aMessage.data.addonInfo };
               let popup = browser.ownerDocument.getElementById("contentAreaContextMenu");
               let event = gContextMenuContentData.event;
               let pos = browser.mapScreenCoordinatesFromContent(event.screenX, event.screenY);
               popup.openPopupAtScreen(pos.x, pos.y, true);
               break;
             }
--- a/browser/components/sessionstore/test/browser_500328.js
+++ b/browser/components/sessionstore/test/browser_500328.js
@@ -37,19 +37,19 @@ function checkState(tab) {
     else if (popStateCount == 1) {
       popStateCount++;
       // When content fires a PopStateEvent and we observe it from a chrome event
       // listener (as we do here, and, thankfully, nowhere else in the tree), the
       // state object will be a cross-compartment wrapper to an object that was
       // deserialized in the content scope. And in this case, since RegExps are
       // not currently Xrayable (see bug 1014991), trying to pull |obj3| (a RegExp)
       // off of an Xrayed Object won't work. So we need to waive.
-      runInContent(tab.linkedBrowser, function(win, event) {
-        return Cu.waiveXrays(event.state).obj3.toString();
-      }, aEvent).then(function(stateStr) {
+      runInContent(tab.linkedBrowser, function(win, state) {
+        return Cu.waiveXrays(state).obj3.toString();
+      }, aEvent.state).then(function(stateStr) {
         is(stateStr, '/^a$/', "second popstate object.");
 
         // Make sure that the new-elem node is present in the document.  If it's
         // not, then this history entry has a different doc identifier than the
         // previous entry, which is bad.
         let doc = contentWindow.document;
         let newElem = doc.getElementById("new-elem");
         ok(newElem, "doc should contain new-elem.");
--- a/dom/animation/AnimationTimeline.cpp
+++ b/dom/animation/AnimationTimeline.cpp
@@ -10,17 +10,17 @@
 #include "nsIPresShell.h"
 #include "nsPresContext.h"
 #include "nsRefreshDriver.h"
 #include "nsDOMNavigationTiming.h"
 
 namespace mozilla {
 namespace dom {
 
-NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(AnimationTimeline, mDocument)
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(AnimationTimeline, mDocument, mWindow)
 
 NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(AnimationTimeline, AddRef)
 NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(AnimationTimeline, Release)
 
 JSObject*
 AnimationTimeline::WrapObject(JSContext* aCx)
 {
   return AnimationTimelineBinding::Wrap(aCx, this);
--- a/dom/animation/AnimationTimeline.h
+++ b/dom/animation/AnimationTimeline.h
@@ -5,43 +5,46 @@
 
 #ifndef mozilla_dom_AnimationTimeline_h
 #define mozilla_dom_AnimationTimeline_h
 
 #include "nsWrapperCache.h"
 #include "nsCycleCollectionParticipant.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/TimeStamp.h"
+#include "nsIGlobalObject.h"
 #include "js/TypeDecls.h"
 #include "nsIDocument.h"
 #include "nsRefreshDriver.h"
 
 struct JSContext;
 
 namespace mozilla {
 namespace dom {
 
 class AnimationTimeline MOZ_FINAL : public nsWrapperCache
 {
 public:
   explicit AnimationTimeline(nsIDocument* aDocument)
     : mDocument(aDocument)
+    , mWindow(aDocument->GetParentObject())
   {
+    MOZ_ASSERT(mWindow);
   }
 
 protected:
   virtual ~AnimationTimeline() { }
 
 public:
   NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(AnimationTimeline)
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(AnimationTimeline)
 
   nsIGlobalObject* GetParentObject() const
   {
-    return mDocument->GetParentObject();
+    return mWindow;
   }
   virtual JSObject* WrapObject(JSContext* aCx) MOZ_OVERRIDE;
 
   // AnimationTimeline methods
   Nullable<TimeDuration> GetCurrentTime() const;
 
   // Wrapper functions for AnimationTimeline DOM methods when called from
   // script.
@@ -65,17 +68,22 @@ public:
   {
     nsRefreshDriver* refreshDriver = GetRefreshDriver();
     return refreshDriver && refreshDriver->IsTestControllingRefreshesEnabled();
   }
 
 protected:
   TimeStamp GetCurrentTimeStamp() const;
 
+  // Sometimes documents can be given a new window, or windows can be given a
+  // new document (e.g. document.open()). Since GetParentObject is required to
+  // _always_ return the same object it can't get the window from our
+  // mDocument, which is why we have pointers to both our document and window.
   nsCOMPtr<nsIDocument> mDocument;
+  nsCOMPtr<nsIGlobalObject> mWindow;
 
   // The most recently used refresh driver time. This is used in cases where
   // we don't have a refresh driver (e.g. because we are in a display:none
   // iframe).
   mutable TimeStamp mLastRefreshDriverTime;
 };
 
 } // namespace dom
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -2285,16 +2285,21 @@ nsDocument::Reset(nsIChannel* aChannel, 
     if (securityManager) {
       securityManager->GetChannelResultPrincipal(aChannel,
                                                  getter_AddRefs(principal));
     }
   }
 
   ResetToURI(uri, aLoadGroup, principal);
 
+  // Note that, since mTiming does not change during a reset, the
+  // navigationStart time remains unchanged and therefore any future new
+  // timeline will have the same global clock time as the old one.
+  mAnimationTimeline = nullptr;
+
   nsCOMPtr<nsIPropertyBag2> bag = do_QueryInterface(aChannel);
   if (bag) {
     nsCOMPtr<nsIURI> baseURI;
     bag->GetPropertyAsInterface(NS_LITERAL_STRING("baseURI"),
                                 NS_GET_IID(nsIURI), getter_AddRefs(baseURI));
     if (baseURI) {
       mDocumentBaseURI = baseURI;
       mChromeXHRDocBaseURI = baseURI;
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -269,17 +269,16 @@ bool nsGlobalWindow::sIdleObserversAPIFu
 static nsIEntropyCollector *gEntropyCollector          = nullptr;
 static int32_t              gRefCnt                    = 0;
 static int32_t              gOpenPopupSpamCount        = 0;
 static PopupControlState    gPopupControlState         = openAbused;
 static int32_t              gRunningTimeoutDepth       = 0;
 static bool                 gMouseDown                 = false;
 static bool                 gDragServiceDisabled       = false;
 static FILE                *gDumpFile                  = nullptr;
-static uint64_t             gNextWindowID              = 0;
 static uint32_t             gSerialCounter             = 0;
 static uint32_t             gTimeoutsRecentlySet       = 0;
 static TimeStamp            gLastRecordedRecentTimeouts;
 #define STATISTICS_INTERVAL (30 * PR_MSEC_PER_SEC)
 
 #ifdef DEBUG_jst
 int32_t gTimeoutCnt                                    = 0;
 #endif
@@ -557,31 +556,38 @@ NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(n
 // Return true if this timeout has a refcount of 1. This is used to check
 // that dummy_timeout doesn't leak from nsGlobalWindow::RunTimeout.
 bool
 nsTimeout::HasRefCntOne()
 {
   return mRefCnt.get() == 1;
 }
 
+namespace mozilla {
+namespace dom {
+extern uint64_t
+NextWindowID();
+}
+}
+
 nsPIDOMWindow::nsPIDOMWindow(nsPIDOMWindow *aOuterWindow)
 : mFrameElement(nullptr), mDocShell(nullptr), mModalStateDepth(0),
   mRunningTimeout(nullptr), mMutationBits(0), mIsDocumentLoaded(false),
   mIsHandlingResizeEvent(false), mIsInnerWindow(aOuterWindow != nullptr),
   mMayHavePaintEventListener(false), mMayHaveTouchEventListener(false),
   mMayHaveTouchCaret(false),
   mMayHaveScrollWheelEventListener(false),
   mMayHaveMouseEnterLeaveEventListener(false),
   mMayHavePointerEnterLeaveEventListener(false),
   mIsModalContentWindow(false),
   mIsActive(false), mIsBackground(false),
   mAudioMuted(false), mAudioVolume(1.0),
   mInnerWindow(nullptr), mOuterWindow(aOuterWindow),
   // Make sure no actual window ends up with mWindowID == 0
-  mWindowID(++gNextWindowID), mHasNotifiedGlobalCreated(false),
+  mWindowID(NextWindowID()), mHasNotifiedGlobalCreated(false),
   mMarkedCCGeneration(0), mSendAfterRemotePaint(false)
  {}
 
 nsPIDOMWindow::~nsPIDOMWindow() {}
 
 // DialogValueHolder CC goop.
 NS_IMPL_CYCLE_COLLECTION(DialogValueHolder, mValue)
 
--- a/dom/base/test/chrome/cpows_parent.xul
+++ b/dom/base/test/chrome/cpows_parent.xul
@@ -192,16 +192,34 @@
     // Make sure errors in this file actually hit window.onerror.
     function recvErrorReportingTest(message) {
       throw "Test Error Probe";
     }
 
     let savedElement = null;
     function recvDomTest(message) {
       savedElement = message.objects.element;
+
+      // Test to ensure that we don't pass CPOWs to C++-implemented interfaces.
+      // See bug 1072980.
+      if (test_state == "remote") {
+        let walker = Components.classes["@mozilla.org/inspector/deep-tree-walker;1"]
+                               .createInstance(Components.interfaces.inIDeepTreeWalker);
+        const SHOW_ELEMENT = Components.interfaces.nsIDOMNodeFilter.SHOW_ELEMENT;
+        walker.showAnonymousContent = true;
+        walker.showSubDocuments = false;
+
+        try {
+          walker.init(savedElement, SHOW_ELEMENT);
+          ok(false, "expected exception passing CPOW to C++");
+        } catch (e) {
+          is(e.result, Components.results.NS_ERROR_XPC_CANT_PASS_CPOW_TO_NATIVE,
+             "got exception when passing CPOW to C++");
+        }
+      }
     }
 
     function recvDomTestAfterGC(message) {
       let id;
       try {
         id = savedElement.id;
       } catch (e) {
         ok(false, "Got exception using DOM element");
--- a/dom/canvas/WebGLContext.cpp
+++ b/dom/canvas/WebGLContext.cpp
@@ -500,73 +500,73 @@ IsFeatureInBlacklist(const nsCOMPtr<nsIG
     if (!NS_SUCCEEDED(gfxInfo->GetFeatureStatus(feature, &status)))
         return false;
 
     return status != nsIGfxInfo::FEATURE_STATUS_OK;
 }
 
 static already_AddRefed<GLContext>
 CreateHeadlessNativeGL(bool forceEnabled, const nsCOMPtr<nsIGfxInfo>& gfxInfo,
-                       bool requireCompatProfile, WebGLContext* webgl)
+                       WebGLContext* webgl)
 {
     if (!forceEnabled &&
         IsFeatureInBlacklist(gfxInfo, nsIGfxInfo::FEATURE_WEBGL_OPENGL))
     {
         webgl->GenerateWarning("Refused to create native OpenGL context"
                                " because of blacklisting.");
         return nullptr;
     }
 
-    nsRefPtr<GLContext> gl = gl::GLContextProvider::CreateHeadless(requireCompatProfile);
+    nsRefPtr<GLContext> gl = gl::GLContextProvider::CreateHeadless();
     if (!gl) {
         webgl->GenerateWarning("Error during native OpenGL init.");
         return nullptr;
     }
     MOZ_ASSERT(!gl->IsANGLE());
 
     return gl.forget();
 }
 
 // Note that we have a separate call for ANGLE and EGL, even though
 // right now, we get ANGLE implicitly by using EGL on Windows.
 // Eventually, we want to be able to pick ANGLE-EGL or native EGL.
 static already_AddRefed<GLContext>
 CreateHeadlessANGLE(bool forceEnabled, const nsCOMPtr<nsIGfxInfo>& gfxInfo,
-                    bool requireCompatProfile, WebGLContext* webgl)
+                    WebGLContext* webgl)
 {
     nsRefPtr<GLContext> gl;
 
 #ifdef XP_WIN
     if (!forceEnabled &&
         IsFeatureInBlacklist(gfxInfo, nsIGfxInfo::FEATURE_WEBGL_ANGLE))
     {
         webgl->GenerateWarning("Refused to create ANGLE OpenGL context"
                                " because of blacklisting.");
         return nullptr;
     }
 
-    gl = gl::GLContextProviderEGL::CreateHeadless(requireCompatProfile);
+    gl = gl::GLContextProviderEGL::CreateHeadless();
     if (!gl) {
         webgl->GenerateWarning("Error during ANGLE OpenGL init.");
         return nullptr;
     }
     MOZ_ASSERT(gl->IsANGLE());
 #endif
 
     return gl.forget();
 }
 
 static already_AddRefed<GLContext>
-CreateHeadlessEGL(bool forceEnabled, bool requireCompatProfile,
+CreateHeadlessEGL(bool forceEnabled, const nsCOMPtr<nsIGfxInfo>& gfxInfo,
                   WebGLContext* webgl)
 {
     nsRefPtr<GLContext> gl;
 
 #ifdef ANDROID
-    gl = gl::GLContextProviderEGL::CreateHeadless(requireCompatProfile);
+    gl = gl::GLContextProviderEGL::CreateHeadless();
     if (!gl) {
         webgl->GenerateWarning("Error during EGL OpenGL init.");
         return nullptr;
     }
     MOZ_ASSERT(!gl->IsANGLE());
 #endif
 
     return gl.forget();
@@ -578,32 +578,26 @@ CreateHeadlessGL(bool forceEnabled, cons
                  WebGLContext* webgl)
 {
     bool preferEGL = PR_GetEnv("MOZ_WEBGL_PREFER_EGL");
     bool disableANGLE = Preferences::GetBool("webgl.disable-angle", false);
 
     if (PR_GetEnv("MOZ_WEBGL_FORCE_OPENGL"))
         disableANGLE = true;
 
-    bool requireCompatProfile = webgl->IsWebGL2() ? false : true;
-
     nsRefPtr<GLContext> gl;
 
     if (preferEGL)
-        gl = CreateHeadlessEGL(forceEnabled, requireCompatProfile, webgl);
+        gl = CreateHeadlessEGL(forceEnabled, gfxInfo, webgl);
 
-    if (!gl && !disableANGLE) {
-        gl = CreateHeadlessANGLE(forceEnabled, gfxInfo, requireCompatProfile,
-                                 webgl);
-    }
+    if (!gl && !disableANGLE)
+        gl = CreateHeadlessANGLE(forceEnabled, gfxInfo, webgl);
 
-    if (!gl) {
-        gl = CreateHeadlessNativeGL(forceEnabled, gfxInfo,
-                                    requireCompatProfile, webgl);
-    }
+    if (!gl)
+        gl = CreateHeadlessNativeGL(forceEnabled, gfxInfo, webgl);
 
     return gl.forget();
 }
 
 // Try to create a dummy offscreen with the given caps.
 static bool
 CreateOffscreenWithCaps(GLContext* gl, const SurfaceCaps& caps)
 {
--- a/dom/canvas/WebGLContext.h
+++ b/dom/canvas/WebGLContext.h
@@ -1204,20 +1204,19 @@ protected:
     bool IsExtensionSupported(WebGLExtensionID ext) const;
 
     static const char* GetExtensionString(WebGLExtensionID ext);
 
     nsTArray<GLenum> mCompressedTextureFormats;
 
     // -------------------------------------------------------------------------
     // WebGL 2 specifics (implemented in WebGL2Context.cpp)
-public:
+
     virtual bool IsWebGL2() const = 0;
 
-protected:
     bool InitWebGL2();
 
     // -------------------------------------------------------------------------
     // Validation functions (implemented in WebGLContextValidate.cpp)
     bool CreateOffscreenGL(bool forceEnabled);
     bool InitAndValidateGL();
     bool ResizeBackbuffer(uint32_t width, uint32_t height);
     bool ValidateBlendEquationEnum(GLenum cap, const char* info);
--- a/dom/canvas/WebGLContextGL.cpp
+++ b/dom/canvas/WebGLContextGL.cpp
@@ -2102,16 +2102,17 @@ WebGLContext::ReadPixels(GLint x, GLint 
                      + bytesPerPixel * subrect_x_in_dest_buffer, // destination
                    subrect_data.get() + subrect_alignedRowSize * y_inside_subrect, // source
                    subrect_plainRowSize); // size
         }
     }
 
     // if we're reading alpha, we may need to do fixup.  Note that we don't allow
     // GL_ALPHA to readpixels currently, but we had the code written for it already.
+
     const bool formatHasAlpha = format == LOCAL_GL_ALPHA ||
                                 format == LOCAL_GL_RGBA;
     if (!formatHasAlpha)
         return;
 
     bool needAlphaFilled;
     if (mBoundReadFramebuffer) {
         needAlphaFilled = !mBoundReadFramebuffer->ColorAttachment(0).HasAlpha();
--- a/dom/canvas/WebGLContextUtils.cpp
+++ b/dom/canvas/WebGLContextUtils.cpp
@@ -1112,22 +1112,21 @@ WebGLContext::AssertCachedState()
 
     GLfloat depthClearValue = 0.0f;
     gl->fGetFloatv(LOCAL_GL_DEPTH_CLEAR_VALUE, &depthClearValue);
     MOZ_ASSERT(IsCacheCorrect(mDepthClearValue, depthClearValue));
 
     AssertUintParamCorrect(gl, LOCAL_GL_STENCIL_CLEAR_VALUE, mStencilClearValue);
 
     GLint stencilBits = 0;
-    if (GetStencilBits(&stencilBits)) {
-        const GLuint stencilRefMask = (1 << stencilBits) - 1;
+    gl->fGetIntegerv(LOCAL_GL_STENCIL_BITS, &stencilBits);
+    const GLuint stencilRefMask = (1 << stencilBits) - 1;
 
-        AssertMaskedUintParamCorrect(gl, LOCAL_GL_STENCIL_REF,      stencilRefMask, mStencilRefFront);
-        AssertMaskedUintParamCorrect(gl, LOCAL_GL_STENCIL_BACK_REF, stencilRefMask, mStencilRefBack);
-    }
+    AssertMaskedUintParamCorrect(gl, LOCAL_GL_STENCIL_REF,      stencilRefMask, mStencilRefFront);
+    AssertMaskedUintParamCorrect(gl, LOCAL_GL_STENCIL_BACK_REF, stencilRefMask, mStencilRefBack);
 
     AssertUintParamCorrect(gl, LOCAL_GL_STENCIL_VALUE_MASK,      mStencilValueMaskFront);
     AssertUintParamCorrect(gl, LOCAL_GL_STENCIL_BACK_VALUE_MASK, mStencilValueMaskBack);
 
     AssertUintParamCorrect(gl, LOCAL_GL_STENCIL_WRITEMASK,      mStencilWriteMaskFront);
     AssertUintParamCorrect(gl, LOCAL_GL_STENCIL_BACK_WRITEMASK, mStencilWriteMaskBack);
 
     // Viewport
--- a/dom/canvas/WebGLContextValidate.cpp
+++ b/dom/canvas/WebGLContextValidate.cpp
@@ -1774,18 +1774,18 @@ WebGLContext::InitAndValidateGL()
     mCurrentProgram = nullptr;
 
     mBoundDrawFramebuffer = nullptr;
     mBoundReadFramebuffer = nullptr;
     mBoundRenderbuffer = nullptr;
 
     MakeContextCurrent();
 
-    // For OpenGL compat. profiles, we always keep vertex attrib 0 array enabled.
-    if (gl->IsCompatibilityProfile())
+    // on desktop OpenGL, we always keep vertex attrib 0 array enabled
+    if (!gl->IsGLES())
         gl->fEnableVertexAttribArray(0);
 
     if (MinCapabilityMode())
         mGLMaxVertexAttribs = MINVALUE_GL_MAX_VERTEX_ATTRIBS;
     else
         gl->fGetIntegerv(LOCAL_GL_MAX_VERTEX_ATTRIBS, &mGLMaxVertexAttribs);
 
     if (mGLMaxVertexAttribs < 8) {
@@ -1884,17 +1884,17 @@ WebGLContext::InitAndValidateGL()
                 // maxVertexOutputComponents in the OpenGL 3.2 spec.
             }
         }
     }
 
     // Always 1 for GLES2
     mMaxFramebufferColorAttachments = 1;
 
-    if (gl->IsCompatibilityProfile()) {
+    if (!gl->IsGLES()) {
         // gl_PointSize is always available in ES2 GLSL, but has to be
         // specifically enabled on desktop GLSL.
         gl->fEnable(LOCAL_GL_VERTEX_PROGRAM_POINT_SIZE);
 
         /* gl_PointCoord is always available in ES2 GLSL and in newer desktop
          * GLSL versions, but apparently not in OpenGL 2 and apparently not (due
          * to a driver bug) on certain NVIDIA setups. See:
          *   http://www.opengl.org/discussion_boards/ubbthreads.php?ubb=showflat&Number=261472
--- a/dom/html/HTMLInputElement.cpp
+++ b/dom/html/HTMLInputElement.cpp
@@ -6616,68 +6616,63 @@ HTMLInputElement::HasStepMismatch(bool a
     return false;
   }
 
   // Value has to be an integral multiple of step.
   return NS_floorModulo(value - GetStepBase(), step) != Decimal(0);
 }
 
 /**
- * Splits the string on the first "@" character and punycode encodes the first
- * and second parts separately before rejoining them with an "@" and returning
- * the result via the aEncodedEmail out-param. Returns false if there is no
- * "@" caracter, if the "@" character is at the start or end, or if the
- * conversion to punycode fails.
+ * Takes aEmail and attempts to convert everything after the first "@"
+ * character (if anything) to punycode before returning the complete result via
+ * the aEncodedEmail out-param. The aIndexOfAt out-param is set to the index of
+ * the "@" character.
  *
- * This function exists because ConvertUTF8toACE() treats 'username@domain' as
- * a single label, but we need to encode the username and domain parts
- * separately.
+ * If no "@" is found in aEmail, aEncodedEmail is simply set to aEmail and
+ * the aIndexOfAt out-param is set to kNotFound.
+ *
+ * Returns true in all cases unless an attempt to punycode encode fails. If
+ * false is returned, aEncodedEmail has not been set.
+ *
+ * This function exists because ConvertUTF8toACE() splits on ".", meaning that
+ * for 'user.name@sld.tld' it would treat "name@sld" as a label. We want to
+ * encode the domain part only.
  */
 static bool PunycodeEncodeEmailAddress(const nsAString& aEmail,
                                        nsAutoCString& aEncodedEmail,
                                        uint32_t* aIndexOfAt)
 {
   nsAutoCString value = NS_ConvertUTF16toUTF8(aEmail);
-  uint32_t length = value.Length();
-
-  uint32_t atPos = (uint32_t)value.FindChar('@');
-  // Email addresses must contain a '@', but can't begin or end with it.
-  if (atPos == (uint32_t)kNotFound || atPos == 0 || atPos == length - 1) {
-    return false;
+  *aIndexOfAt = (uint32_t)value.FindChar('@');
+
+  if (*aIndexOfAt == (uint32_t)kNotFound ||
+      *aIndexOfAt == value.Length() - 1) {
+    aEncodedEmail = value;
+    return true;
   }
 
   nsCOMPtr<nsIIDNService> idnSrv = do_GetService(NS_IDNSERVICE_CONTRACTID);
   if (!idnSrv) {
     NS_ERROR("nsIIDNService isn't present!");
     return false;
   }
 
-  const nsDependentCSubstring username = Substring(value, 0, atPos);
+  uint32_t indexOfDomain = *aIndexOfAt + 1;
+
+  const nsDependentCSubstring domain = Substring(value, indexOfDomain);
   bool ace;
-  if (NS_SUCCEEDED(idnSrv->IsACE(username, &ace)) && !ace) {
-    nsAutoCString usernameACE;
-    // TODO: Bug 901347: Usernames longer than 63 chars are not converted by
-    // ConvertUTF8toACE(). For now, continue on even if the conversion fails.
-    if (NS_SUCCEEDED(idnSrv->ConvertUTF8toACE(username, usernameACE))) {
-      value.Replace(0, atPos, usernameACE);
-      atPos = usernameACE.Length();
-    }
-  }
-
-  const nsDependentCSubstring domain = Substring(value, atPos + 1);
   if (NS_SUCCEEDED(idnSrv->IsACE(domain, &ace)) && !ace) {
     nsAutoCString domainACE;
     if (NS_FAILED(idnSrv->ConvertUTF8toACE(domain, domainACE))) {
       return false;
     }
-    value.Replace(atPos + 1, domain.Length(), domainACE);
+    value.Replace(indexOfDomain, domain.Length(), domainACE);
   }
 
   aEncodedEmail = value;
-  *aIndexOfAt = atPos;
   return true;
 }
 
 bool
 HTMLInputElement::HasBadInput() const
 {
   if (mType == NS_FORM_INPUT_NUMBER) {
     nsAutoString value;
@@ -7117,18 +7112,20 @@ HTMLInputElement::IsValidEmailAddress(co
 {
   // Email addresses can't be empty and can't end with a '.' or '-'.
   if (aValue.IsEmpty() || aValue.Last() == '.' || aValue.Last() == '-') {
     return false;
   }
 
   uint32_t atPos;
   nsAutoCString value;
-  // This call also checks whether aValue contains a correctly-placed '@' sign.
-  if (!PunycodeEncodeEmailAddress(aValue, value, &atPos)) {
+  if (!PunycodeEncodeEmailAddress(aValue, value, &atPos) ||
+      atPos == (uint32_t)kNotFound || atPos == 0 || atPos == value.Length() - 1) {
+    // Could not encode, or "@" was not found, or it was at the start or end
+    // of the input - in all cases, not a valid email address.
     return false;
   }
 
   uint32_t length = value.Length();
   uint32_t i = 0;
 
   // Parsing the username.
   for (; i < atPos; ++i) {
--- a/dom/html/test/forms/test_input_email.html
+++ b/dom/html/test/forms/test_input_email.html
@@ -31,179 +31,189 @@ function invalidEventHandler(e)
 {
   is(e.type, "invalid", "Invalid event type should be invalid");
   gInvalid = true;
 }
 
 function checkValidEmailAddress(element)
 {
   gInvalid = false;
-  ok(!element.validity.typeMismatch,
-     "Element should not suffer from type mismatch (with value='"+element.value+"')");
+  ok(!element.validity.typeMismatch && !element.validity.badInput,
+     "Element should not suffer from type mismatch or bad input (with value='"+element.value+"')");
   ok(element.validity.valid, "Element should be valid");
   ok(element.checkValidity(), "Element should be valid");
   ok(!gInvalid, "The invalid event should not have been thrown");
   is(element.validationMessage, '',
      "Validation message should be the empty string");
   ok(element.matches(":valid"), ":valid pseudo-class should apply");
 }
 
-function checkInvalidEmailAddress(element)
+const VALID = 0;
+const TYPE_MISMATCH = 1 << 0;
+const BAD_INPUT = 1 << 1;
+
+function checkInvalidEmailAddress(element, failedValidityStates)
 {
+  info("Checking " + element.value);
   gInvalid = false;
-  ok(element.validity.typeMismatch,
-     "Element should suffer from type mismatch (with value='"+element.value+"')");
+  var expectTypeMismatch = !!(failedValidityStates & TYPE_MISMATCH);
+  var expectBadInput = !!(failedValidityStates & BAD_INPUT);
+  ok(element.validity.typeMismatch == expectTypeMismatch,
+     "Element should " + (expectTypeMismatch ? "" : "not ") + "suffer from type mismatch (with value='"+element.value+"')");
+  ok(element.validity.badInput == expectBadInput,
+     "Element should " + (expectBadInput ? "" : "not ") + "suffer from bad input (with value='"+element.value+"')");
   ok(!element.validity.valid, "Element should not be valid");
   ok(!element.checkValidity(), "Element should not be valid");
   ok(gInvalid, "The invalid event should have been thrown");
   is(element.validationMessage, "Please enter an email address.",
      "Validation message is not valid");
   ok(element.matches(":invalid"), ":invalid pseudo-class should apply");
 }
 
-function testEmailAddress(aElement, aValue, aMultiple, aValidity)
+function testEmailAddress(aElement, aValue, aMultiple, aValidityFailures)
 {
   aElement.multiple = aMultiple;
   aElement.value = aValue;
 
-  if (aValidity) {
+  if (!aValidityFailures) {
     checkValidEmailAddress(aElement);
   } else {
-    checkInvalidEmailAddress(aElement);
+    checkInvalidEmailAddress(aElement, aValidityFailures);
   }
 }
 
 var email = document.forms[0].elements[0];
 
 // Simple values, checking the e-mail syntax validity.
 var values = [
-  [ '', true ], // The empty string shouldn't be considered as invalid.
-  [ 'foo@bar.com', true ],
-  [ ' foo@bar.com', true ],
-  [ 'foo@bar.com ', true ],
-  [ '\r\n foo@bar.com', true ],
-  [ 'foo@bar.com \n\r', true ],
-  [ '\n\n \r\rfoo@bar.com\n\n   \r\r', true ],
-  [ '\n\r \n\rfoo@bar.com\n\r   \n\r', true ],
-  [ 'tulip', false ],
+  [ '' ], // The empty string shouldn't be considered as invalid.
+  [ 'foo@bar.com', VALID ],
+  [ ' foo@bar.com', VALID ],
+  [ 'foo@bar.com ', VALID ],
+  [ '\r\n foo@bar.com', VALID ],
+  [ 'foo@bar.com \n\r', VALID ],
+  [ '\n\n \r\rfoo@bar.com\n\n   \r\r', VALID ],
+  [ '\n\r \n\rfoo@bar.com\n\r   \n\r', VALID ],
+  [ 'tulip', TYPE_MISMATCH ],
   // Some checks on the user part of the address.
-  [ '@bar.com', false ],
-  [ 'f\noo@bar.com', true ],
-  [ 'f\roo@bar.com', true ],
-  [ 'f\r\noo@bar.com', true ],
-  [ 'fü@foo.com', true ],
+  [ '@bar.com', TYPE_MISMATCH ],
+  [ 'f\noo@bar.com', VALID ],
+  [ 'f\roo@bar.com', VALID ],
+  [ 'f\r\noo@bar.com', VALID ],
+  [ 'fü@foo.com', TYPE_MISMATCH ],
   // Some checks for the domain part.
-  [ 'foo@bar', true ],
-  [ 'foo@b', true ],
-  [ 'foo@', false ],
-  [ 'foo@bar.', false ],
-  [ 'foo@foo.bar', true ],
-  [ 'foo@foo..bar', false ],
-  [ 'foo@.bar', false ],
-  [ 'foo@tulip.foo.bar', true ],
-  [ 'foo@tulip.foo-bar', true ],
-  [ 'foo@1.2', true ],
-  [ 'foo@127.0.0.1', true ],
-  [ 'foo@1.2.3', true ],
-  [ 'foo@b\nar.com', true ],
-  [ 'foo@b\rar.com', true ],
-  [ 'foo@b\r\nar.com', true ],
-  [ 'foo@.', false ],
-  [ 'foo@fü.com', true ],
-  [ 'foo@fu.cüm', true ],
-  // Long strings with UTF-8.
-  [ 'this.is.email.should.be.longer.than.sixty.four.characters.föö@mözillä.tld', true ],
-  [ 'this-is-email-should-be-longer-than-sixty-four-characters-föö@mözillä.tld', true, true ],
-  // Long labels.
-  [ 'foo@thislabelisexactly63characterssssssssssssssssssssssssssssssssss', true ],
-  [ 'foo@thislabelisexactly63characterssssssssssssssssssssssssssssssssss.com', true ],
-  [ 'foo@foo.thislabelisexactly63characterssssssssssssssssssssssssssssssssss.com', true ],
-  [ 'foo@foo.thislabelisexactly63characterssssssssssssssssssssssssssssssssss', true ],
-  [ 'foo@thislabelisexactly64charactersssssssssssssssssssssssssssssssssss', false ],
-  [ 'foo@thislabelisexactly64charactersssssssssssssssssssssssssssssssssss.com', false ],
-  [ 'foo@foo.thislabelisexactly64charactersssssssssssssssssssssssssssssssssss.com', false ],
-  [ 'foo@foo.thislabelisexactly64charactersssssssssssssssssssssssssssssssssss', false ],
-  // Long labels with UTF-8.
-  [ 'foo@thisläbelisexäctly63charäcterssssssssssssssssssssssssssssssssss', false ],
-  [ 'foo@thisläbelisexäctly63charäcterssssssssssssssssssssssssssssssssss.com', false ],
-  [ 'foo@foo.thisläbelisexäctly63charäcterssssssssssssssssssssssssssssssssss.com', false ],
-  [ 'foo@foo.thisläbelisexäctly63charäcterssssssssssssssssssssssssssssssssss', false ],
+  [ 'foo@bar', VALID ],
+  [ 'foo@b', VALID ],
+  [ 'foo@', TYPE_MISMATCH ],
+  [ 'foo@bar.', TYPE_MISMATCH ],
+  [ 'foo@foo.bar', VALID ],
+  [ 'foo@foo..bar', TYPE_MISMATCH ],
+  [ 'foo@.bar', TYPE_MISMATCH ],
+  [ 'foo@tulip.foo.bar', VALID ],
+  [ 'foo@tulip.foo-bar', VALID ],
+  [ 'foo@1.2', VALID ],
+  [ 'foo@127.0.0.1', VALID ],
+  [ 'foo@1.2.3', VALID ],
+  [ 'foo@b\nar.com', VALID ],
+  [ 'foo@b\rar.com', VALID ],
+  [ 'foo@b\r\nar.com', VALID ],
+  [ 'foo@.', TYPE_MISMATCH ],
+  [ 'foo@fü.com', VALID ],
+  [ 'foo@fu.cüm', VALID ],
+  [ 'thisUsernameIsLongerThanSixtyThreeCharactersInLengthRightAboutNow@mozilla.tld', VALID ],
+  // Long strings with UTF-8 in username.
+  [ 'this.is.email.should.be.longer.than.sixty.four.characters.föö@mözillä.tld', TYPE_MISMATCH ],
+  [ 'this-is-email-should-be-longer-than-sixty-four-characters-föö@mözillä.tld', TYPE_MISMATCH, true ],
+  // Long labels (labels greater than 63 chars long are not allowed).
+  [ 'foo@thislabelisexactly63characterssssssssssssssssssssssssssssssssss', VALID ],
+  [ 'foo@thislabelisexactly63characterssssssssssssssssssssssssssssssssss.com', VALID ],
+  [ 'foo@foo.thislabelisexactly63characterssssssssssssssssssssssssssssssssss.com', VALID ],
+  [ 'foo@foo.thislabelisexactly63characterssssssssssssssssssssssssssssssssss', VALID ],
+  [ 'foo@thislabelisexactly64charactersssssssssssssssssssssssssssssssssss', TYPE_MISMATCH | BAD_INPUT ],
+  [ 'foo@thislabelisexactly64charactersssssssssssssssssssssssssssssssssss.com', TYPE_MISMATCH | BAD_INPUT ],
+  [ 'foo@foo.thislabelisexactly64charactersssssssssssssssssssssssssssssssssss.com', TYPE_MISMATCH | BAD_INPUT ],
+  [ 'foo@foo.thislabelisexactly64charactersssssssssssssssssssssssssssssssssss', TYPE_MISMATCH | BAD_INPUT ],
+  // Long labels with UTF-8 (punycode encoding will increase the label to more than 63 chars).
+  [ 'foo@thisläbelisexäctly63charäcterssssssssssssssssssssssssssssssssss', TYPE_MISMATCH | BAD_INPUT ],
+  [ 'foo@thisläbelisexäctly63charäcterssssssssssssssssssssssssssssssssss.com', TYPE_MISMATCH | BAD_INPUT ],
+  [ 'foo@foo.thisläbelisexäctly63charäcterssssssssssssssssssssssssssssssssss.com', TYPE_MISMATCH | BAD_INPUT ],
+  [ 'foo@foo.thisläbelisexäctly63charäcterssssssssssssssssssssssssssssssssss', TYPE_MISMATCH | BAD_INPUT ],
   // The domains labels (sub-domains or tld) can't start or finish with a '-'
-  [ 'foo@foo-bar', true ],
-  [ 'foo@-foo', false ],
-  [ 'foo@foo-.bar', false ],
-  [ 'foo@-.-', false ],
-  [ 'foo@fo-o.bar', true ],
-  [ 'foo@fo-o.-bar', false ],
-  [ 'foo@fo-o.bar-', false ],
-  [ 'foo@fo-o.-', false ],
-  [ 'foo@fo--o', true ],
+  [ 'foo@foo-bar', VALID ],
+  [ 'foo@-foo', TYPE_MISMATCH ],
+  [ 'foo@foo-.bar', TYPE_MISMATCH ],
+  [ 'foo@-.-', TYPE_MISMATCH ],
+  [ 'foo@fo-o.bar', VALID ],
+  [ 'foo@fo-o.-bar', TYPE_MISMATCH ],
+  [ 'foo@fo-o.bar-', TYPE_MISMATCH ],
+  [ 'foo@fo-o.-', TYPE_MISMATCH ],
+  [ 'foo@fo--o', VALID ],
 ];
 
 // Multiple values, we don't check e-mail validity, only multiple stuff.
 var multipleValues = [
-  [ 'foo@bar.com, foo@bar.com', true ],
-  [ 'foo@bar.com,foo@bar.com', true ],
-  [ 'foo@bar.com,foo@bar.com,foo@bar.com', true ],
-  [ '     foo@bar.com     ,     foo@bar.com    ', true ],
-  [ '\tfoo@bar.com\t,\tfoo@bar.com\t', true ],
-  [ '\rfoo@bar.com\r,\rfoo@bar.com\r', true ],
-  [ '\nfoo@bar.com\n,\nfoo@bar.com\n', true ],
-  [ '\ffoo@bar.com\f,\ffoo@bar.com\f', true ],
-  [ '\t foo@bar.com\r,\nfoo@bar.com\f', true ],
-  [ 'foo@b,ar.com,foo@bar.com', false ],
-  [ 'foo@bar.com,foo@bar.com,', false ],
-  [ '   foo@bar.com   ,   foo@bar.com   ,   ', false ],
-  [ ',foo@bar.com,foo@bar.com', false ],
-  [ ',foo@bar.com,foo@bar.com', false ],
-  [ 'foo@bar.com,,,foo@bar.com', false ],
-  [ 'foo@bar.com;foo@bar.com', false ],
-  [ '<foo@bar.com>, <foo@bar.com>', false ],
-  [ 'foo@bar, foo@bar.com', true ],
-  [ 'foo@bar.com, foo', false ],
-  [ 'foo, foo@bar.com', false ],
+  [ 'foo@bar.com, foo@bar.com', VALID ],
+  [ 'foo@bar.com,foo@bar.com', VALID ],
+  [ 'foo@bar.com,foo@bar.com,foo@bar.com', VALID ],
+  [ '     foo@bar.com     ,     foo@bar.com    ', VALID ],
+  [ '\tfoo@bar.com\t,\tfoo@bar.com\t', VALID ],
+  [ '\rfoo@bar.com\r,\rfoo@bar.com\r', VALID ],
+  [ '\nfoo@bar.com\n,\nfoo@bar.com\n', VALID ],
+  [ '\ffoo@bar.com\f,\ffoo@bar.com\f', VALID ],
+  [ '\t foo@bar.com\r,\nfoo@bar.com\f', VALID ],
+  [ 'foo@b,ar.com,foo@bar.com', TYPE_MISMATCH ],
+  [ 'foo@bar.com,foo@bar.com,', TYPE_MISMATCH ],
+  [ '   foo@bar.com   ,   foo@bar.com   ,   ', TYPE_MISMATCH ],
+  [ ',foo@bar.com,foo@bar.com', TYPE_MISMATCH ],
+  [ ',foo@bar.com,foo@bar.com', TYPE_MISMATCH ],
+  [ 'foo@bar.com,,,foo@bar.com', TYPE_MISMATCH ],
+  [ 'foo@bar.com;foo@bar.com', TYPE_MISMATCH ],
+  [ '<foo@bar.com>, <foo@bar.com>', TYPE_MISMATCH ],
+  [ 'foo@bar, foo@bar.com', VALID ],
+  [ 'foo@bar.com, foo', TYPE_MISMATCH ],
+  [ 'foo, foo@bar.com', TYPE_MISMATCH ],
 ];
 
 /* Additional username checks. */
 
 var legalCharacters = "abcdefghijklmnopqrstuvwxyz";
 legalCharacters += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
 legalCharacters += "0123456789";
 legalCharacters += "!#$%&'*+-/=?^_`{|}~.";
 
 // Add all username legal characters individually to the list.
 for (c of legalCharacters) {
-  values.push([c + "@bar.com", true]);
+  values.push([c + "@bar.com", VALID]);
 }
 // Add the concatenation of all legal characters too.
-values.push([legalCharacters + "@bar.com", true]);
+values.push([legalCharacters + "@bar.com", VALID]);
 
 // Add username illegal characters, the same way.
 var illegalCharacters = "()<>[]:;@\\, \t";
 for (c of illegalCharacters) {
-  values.push([illegalCharacters + "@bar.com", false]);
+  values.push([illegalCharacters + "@bar.com", TYPE_MISMATCH]);
 }
 
 /* Additional domain checks. */
 
 legalCharacters = "abcdefghijklmnopqrstuvwxyz";
 legalCharacters += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
 legalCharacters += "0123456789";
 
 // Add domain legal characters (except '.' and '-' because they are special).
 for (c of legalCharacters) {
-  values.push(["foo@foo.bar" + c, true]);
+  values.push(["foo@foo.bar" + c, VALID]);
 }
 // Add the concatenation of all legal characters too.
-values.push(["foo@bar." + legalCharacters, true]);
+values.push(["foo@bar." + legalCharacters, VALID]);
 
 // Add domain illegal characters.
 illegalCharacters = "()<>[]:;@\\,!#$%&'*+/=?^_`{|}~ \t";
 for (c of illegalCharacters) {
-  values.push(['foo@foo.ba' + c + 'r', false]);
+  values.push(['foo@foo.ba' + c + 'r', TYPE_MISMATCH]);
 }
 
 values.forEach(function([value, valid, todo]) {
   if (todo === true) {
     email.value = value;
     todo_is(email.validity.valid, true, "value should be valid");
   } else {
     testEmailAddress(email, value, false, valid);
@@ -212,16 +222,16 @@ values.forEach(function([value, valid, t
 
 multipleValues.forEach(function([value, valid]) {
   testEmailAddress(email, value, true, valid);
 });
 
 // Make sure setting multiple changes the value.
 email.multiple = false;
 email.value = "foo@bar.com, foo@bar.com";
-checkInvalidEmailAddress(email);
+checkInvalidEmailAddress(email, TYPE_MISMATCH);
 email.multiple = true;
 checkValidEmailAddress(email);
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/ipc/ContentChild.cpp
+++ b/dom/ipc/ContentChild.cpp
@@ -2519,16 +2519,51 @@ ContentChild::GetBrowserOrId(TabChild* a
         this == aTabChild->Manager()) {
         return PBrowserOrId(aTabChild);
     }
     else {
         return PBrowserOrId(aTabChild->GetTabId());
     }
 }
 
+// This code goes here rather than nsGlobalWindow.cpp because nsGlobalWindow.cpp
+// can't include ContentChild.h since it includes windows.h.
+
+static uint64_t gNextWindowID = 0;
+
+// We use only 53 bits for the window ID so that it can be converted to and from
+// a JS value without loss of precision. The upper bits of the window ID hold the
+// process ID. The lower bits identify the window.
+static const uint64_t kWindowIDTotalBits = 53;
+static const uint64_t kWindowIDProcessBits = 22;
+static const uint64_t kWindowIDWindowBits = kWindowIDTotalBits - kWindowIDProcessBits;
+
+// Try to return a window ID that is unique across processes and that will never
+// be recycled.
+uint64_t
+NextWindowID()
+{
+  uint64_t processID = 0;
+  if (XRE_GetProcessType() == GeckoProcessType_Content) {
+    ContentChild* cc = ContentChild::GetSingleton();
+    processID = cc->GetID();
+  }
+
+  MOZ_RELEASE_ASSERT(processID < (uint64_t(1) << kWindowIDProcessBits));
+  uint64_t processBits = processID & ((uint64_t(1) << kWindowIDProcessBits) - 1);
+
+  // Make sure no actual window ends up with mWindowID == 0.
+  uint64_t windowID = ++gNextWindowID;
+
+  MOZ_RELEASE_ASSERT(windowID < (uint64_t(1) << kWindowIDWindowBits));
+  uint64_t windowBits = windowID & ((uint64_t(1) << kWindowIDWindowBits) - 1);
+
+  return (processBits << kWindowIDWindowBits) | windowBits;
+}
+
 } // namespace dom
 } // namespace mozilla
 
 extern "C" {
 
 #if defined(MOZ_NUWA_PROCESS)
 NS_EXPORT void
 GetProtoFdInfos(NuwaProtoFdInfo* aInfoList,
--- a/dom/ipc/ContentChild.h
+++ b/dom/ipc/ContentChild.h
@@ -476,12 +476,15 @@ private:
     bool mIsAlive;
     nsString mProcessName;
 
     static ContentChild* sSingleton;
 
     DISALLOW_EVIL_CONSTRUCTORS(ContentChild);
 };
 
+uint64_t
+NextWindowID();
+
 } // namespace dom
 } // namespace mozilla
 
 #endif
--- a/dom/ipc/PPluginWidget.ipdl
+++ b/dom/ipc/PPluginWidget.ipdl
@@ -31,30 +31,34 @@ sync protocol PPluginWidget {
 
 parent:
   async __delete__();
 
 parent:
   async Create();
   async Destroy();
   async SetFocus(bool aRaise);
-  async Invalidate(nsIntRect aRect);
 
   /**
    * Returns NS_NATIVE_PLUGIN_PORT and its variants: a sharable native
    * window for plugins. On Linux, this returns an XID for a socket widget
    * embedded in the chrome side native window. On Windows this returns the
    * native HWND of the plugin widget.
    */
   sync GetNativePluginPort() returns (uintptr_t value);
 
+child:
   /**
-   * nsIWidget interfaces we'll need until this information flows
-   * over the compositor connection.
+   * Sent from content when a plugin is unloaded via layout. We shut down
+   * some things in response to this so that we don't end up firing async
+   * msgs after the child is destroyed. We know that after this call
+   * the child actor may not be on the other side.
    */
-  async Show(bool aState);
-  async Resize(nsIntRect aRect);
-  async Move(double aX, double aY);
-  async SetWindowClipRegion(nsIntRect[] Regions, bool aIntersectWithExisting);
+  async ParentShutdown();
+
+  /**
+   * Requests a full update of the plugin window.
+   */
+  async UpdateWindow(uintptr_t aChildId);
 };
 
 }
 }
--- a/dom/ipc/TabParent.cpp
+++ b/dom/ipc/TabParent.cpp
@@ -361,16 +361,25 @@ TabParent::Destroy()
     RemoveTabParentFromTable(frame->GetLayersId());
     frame->Destroy();
   }
   mIsDestroyed = true;
 
   if (XRE_GetProcessType() == GeckoProcessType_Default) {
     Manager()->AsContentParent()->NotifyTabDestroying(this);
   }
+
+  // Let all PluginWidgets know we are tearing down. Prevents
+  // these objects from sending async events after the child side
+  // is shut down.
+  const nsTArray<PPluginWidgetParent*>& kids = ManagedPPluginWidgetParent();
+  for (uint32_t idx = 0; idx < kids.Length(); ++idx) {
+      static_cast<mozilla::plugins::PluginWidgetParent*>(kids[idx])->ParentDestroy();
+  }
+
   mMarkedDestroying = true;
 }
 
 bool
 TabParent::Recv__delete__()
 {
   if (XRE_GetProcessType() == GeckoProcessType_Default) {
     Manager()->AsContentParent()->NotifyTabDestroyed(this, mMarkedDestroying);
--- a/dom/media/fmp4/android/AndroidDecoderModule.cpp
+++ b/dom/media/fmp4/android/AndroidDecoderModule.cpp
@@ -168,17 +168,17 @@ public:
   }
 
 protected:
   bool EnsureGLContext() {
     if (mGLContext) {
       return true;
     }
 
-    mGLContext = GLContextProvider::CreateHeadless(false);
+    mGLContext = GLContextProvider::CreateHeadless();
     return mGLContext;
   }
 
   layers::ImageContainer* mImageContainer;
   const mp4_demuxer::VideoDecoderConfig& mConfig;
   RefPtr<AndroidSurfaceTexture> mSurfaceTexture;
   nsRefPtr<GLContext> mGLContext;
 };
--- a/dom/media/mediasource/ResourceQueue.h
+++ b/dom/media/mediasource/ResourceQueue.h
@@ -103,17 +103,17 @@ public:
     Push(new ResourceItem(aData));
   }
 
   // Tries to evict at least aSizeToEvict from the queue up until
   // aOffset. Returns amount evicted.
   uint32_t Evict(uint64_t aOffset, uint32_t aSizeToEvict) {
     SBR_DEBUG("ResourceQueue(%p)::Evict(aOffset=%llu, aSizeToEvict=%u)",
               this, aOffset, aSizeToEvict);
-    return EvictBefore(std::min(aOffset, (uint64_t)aSizeToEvict));
+    return EvictBefore(std::min(aOffset, mOffset + (uint64_t)aSizeToEvict));
   }
 
   uint32_t EvictBefore(uint64_t aOffset) {
     SBR_DEBUG("ResourceQueue(%p)::EvictBefore(%llu)", this, aOffset);
     uint32_t evicted = 0;
     while (ResourceItem* item = ResourceAt(0)) {
       SBR_DEBUG("ResourceQueue(%p)::EvictBefore item=%p length=%d offset=%llu",
                 this, item, item->mData->Length(), mOffset);
--- a/dom/media/tests/mochitest/constraints.js
+++ b/dom/media/tests/mochitest/constraints.js
@@ -65,30 +65,23 @@ var common_tests = [
 
 
 /**
  * Starts the test run by running through each constraint
  * test by verifying that the right resolution and rejection is fired.
  */
 
 function testConstraints(tests) {
-  function testgum(p, test) {
-    return p.then(function() {
-      return navigator.mediaDevices.getUserMedia(test.constraints);
-    })
-    .then(function() {
-      is(null, test.error, test.message);
-    }, function(reason) {
-      is(reason.name, test.error, test.message + ": " + reason.message);
-    });
+  function testgum(prev, test) {
+    return prev.then(() => navigator.mediaDevices.getUserMedia(test.constraints))
+      .then(() => is(null, test.error, test.message),
+            reason => is(reason.name, test.error, test.message + ": " + reason.message));
   }
 
-  var p = new Promise(r => SpecialPowers.pushPrefEnv({
-      set : [ ['media.getusermedia.browser.enabled', false],
-              ['media.getusermedia.screensharing.enabled', false] ]
-    }, r));
+  var p = new Promise(resolve => SpecialPowers.pushPrefEnv({
+    set : [ ['media.getusermedia.browser.enabled', false],
+            ['media.getusermedia.screensharing.enabled', false] ]
+  }, resolve));
 
-  tests.forEach(function(test) {
-    p = testgum(p, test);
-  });
-  p.catch(reason => ok(false, "Unexpected failure: " + reason.message))
-  .then(SimpleTest.finish);
+  tests.reduce(testgum, p)
+    .catch(reason => ok(false, "Unexpected failure: " + reason.message))
+    .then(SimpleTest.finish);
 }
--- a/dom/media/tests/mochitest/dataChannel.js
+++ b/dom/media/tests/mochitest/dataChannel.js
@@ -1,230 +1,170 @@
 /* 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/. */
 
+/**
+ * Returns the contents of a blob as text
+ *
+ * @param {Blob} blob
+          The blob to retrieve the contents from
+ */
+function getBlobContent(blob) {
+  return new Promise(resolve => {
+    var reader = new FileReader();
+    // Listen for 'onloadend' which will always be called after a success or failure
+    reader.onloadend = event => resolve(event.target.result);
+    reader.readAsText(blob);
+  });
+}
+
 function addInitialDataChannel(chain) {
   chain.insertBefore('PC_LOCAL_CREATE_OFFER', [
-    ['PC_LOCAL_CREATE_DATA_CHANNEL',
-      function (test) {
-        var channel = test.pcLocal.createDataChannel({});
-
-        is(channel.binaryType, "blob", channel + " is of binary type 'blob'");
-        is(channel.readyState, "connecting", channel + " is in state: 'connecting'");
+    function PC_REMOTE_EXPECT_DATA_CHANNEL(test) {
+      test.pcRemote.expectDataChannel();
+    },
 
-        is(test.pcLocal.signalingState, STABLE,
-           "Create datachannel does not change signaling state");
+    function PC_LOCAL_CREATE_DATA_CHANNEL(test) {
+      var channel = test.pcLocal.createDataChannel({});
+      is(channel.binaryType, "blob", channel + " is of binary type 'blob'");
+      is(channel.readyState, "connecting", channel + " is in state: 'connecting'");
 
-        test.next();
-      }
-    ]
+      is(test.pcLocal.signalingState, STABLE,
+         "Create datachannel does not change signaling state");
+    }
   ]);
-  chain.insertAfter('PC_REMOTE_CREATE_ANSWER', [
-    [
-      'PC_LOCAL_SETUP_DATA_CHANNEL_CALLBACK',
-      function (test) {
-        test.waitForInitialDataChannel(test.pcLocal, function () {
-          ok(true, test.pcLocal + " dataChannels[0] switched to 'open'");
-        },
-        // At this point a timeout failure will be of no value
-        null);
-        test.next();
-      }
-    ],
-    [
-      'PC_REMOTE_SETUP_DATA_CHANNEL_CALLBACK',
-      function (test) {
-        test.waitForInitialDataChannel(test.pcRemote, function () {
-          ok(true, test.pcRemote + " dataChannels[0] switched to 'open'");
-        },
-        // At this point a timeout failure will be of no value
-        null);
-        test.next();
-      }
-    ]
-  ]);
+
   chain.insertBefore('PC_LOCAL_CHECK_MEDIA_TRACKS', [
-    [
-      'PC_LOCAL_VERIFY_DATA_CHANNEL_STATE',
-      function (test) {
-        test.waitForInitialDataChannel(test.pcLocal, function() {
-          test.next();
-        }, function() {
-          ok(false, test.pcLocal + " initial dataChannels[0] failed to switch to 'open'");
-          //TODO: use stopAndExit() once bug 1019323 has landed
-          unexpectedEventAndFinish(this, 'timeout')
-          // to prevent test framework timeouts
-          test.next();
-        });
-      }
-    ],
-    [
-      'PC_REMOTE_VERIFY_DATA_CHANNEL_STATE',
-      function (test) {
-        test.waitForInitialDataChannel(test.pcRemote, function() {
-          test.next();
-        }, function() {
-          ok(false, test.pcRemote + " initial dataChannels[0] failed to switch to 'open'");
-          //TODO: use stopAndExit() once bug 1019323 has landed
-          unexpectedEventAndFinish(this, 'timeout');
-          // to prevent test framework timeouts
-          test.next();
-        });
-      }
-    ]
+    function PC_LOCAL_VERIFY_DATA_CHANNEL_STATE(test) {
+      return test.pcLocal.dataChannels[0].opened;
+    },
+
+    function PC_REMOTE_VERIFY_DATA_CHANNEL_STATE(test) {
+      return test.pcRemote.nextDataChannel.then(channel => channel.opened);
+    }
   ]);
   chain.removeAfter('PC_REMOTE_CHECK_ICE_CONNECTIONS');
   chain.append([
-    [
-      'SEND_MESSAGE',
-      function (test) {
-        var message = "Lorem ipsum dolor sit amet";
+    function SEND_MESSAGE(test) {
+      var message = "Lorem ipsum dolor sit amet";
 
-        test.send(message, function (channel, data) {
-          is(data, message, "Message correctly transmitted from pcLocal to pcRemote.");
+      return test.send(message).then(result => {
+        is(result.data, message, "Message correctly transmitted from pcLocal to pcRemote.");
+      });
+    },
 
-          test.next();
-        });
-      }
-    ],
-    [
-      'SEND_BLOB',
-      function (test) {
-        var contents = ["At vero eos et accusam et justo duo dolores et ea rebum."];
-        var blob = new Blob(contents, { "type" : "text/plain" });
+    function SEND_BLOB(test) {
+      var contents = ["At vero eos et accusam et justo duo dolores et ea rebum."];
+      var blob = new Blob(contents, { "type" : "text/plain" });
 
-        test.send(blob, function (channel, data) {
-          ok(data instanceof Blob, "Received data is of instance Blob");
-          is(data.size, blob.size, "Received data has the correct size.");
+      return test.send(blob).then(result => {
+        ok(result.data instanceof Blob, "Received data is of instance Blob");
+        is(result.data.size, blob.size, "Received data has the correct size.");
 
-          getBlobContent(data, function (recv_contents) {
-            is(recv_contents, contents, "Received data has the correct content.");
+        return getBlobContent(result.data);
+      }).then(recv_contents =>
+              is(recv_contents, contents, "Received data has the correct content."));
+    },
 
-            test.next();
-          });
-        });
-      }
-    ],
-    [
-      'CREATE_SECOND_DATA_CHANNEL',
-      function (test) {
-        test.createDataChannel({ }, function (sourceChannel, targetChannel) {
-          is(sourceChannel.readyState, "open", sourceChannel + " is in state: 'open'");
-          is(targetChannel.readyState, "open", targetChannel + " is in state: 'open'");
+    function CREATE_SECOND_DATA_CHANNEL(test) {
+      return test.createDataChannel({ }).then(result => {
+        var sourceChannel = result.local;
+        var targetChannel = result.remote;
+        is(sourceChannel.readyState, "open", sourceChannel + " is in state: 'open'");
+        is(targetChannel.readyState, "open", targetChannel + " is in state: 'open'");
+
+        is(targetChannel.binaryType, "blob", targetChannel + " is of binary type 'blob'");
+      });
+    },
 
-          is(targetChannel.binaryType, "blob", targetChannel + " is of binary type 'blob'");
-          is(targetChannel.readyState, "open", targetChannel + " is in state: 'open'");
+    function SEND_MESSAGE_THROUGH_LAST_OPENED_CHANNEL(test) {
+      var channels = test.pcRemote.dataChannels;
+      var message = "I am the Omega";
 
-          test.next();
-        });
-      }
-    ],
-    [
-      'SEND_MESSAGE_THROUGH_LAST_OPENED_CHANNEL',
-      function (test) {
-        var channels = test.pcRemote.dataChannels;
-        var message = "Lorem ipsum dolor sit amet";
+      return test.send(message).then(result => {
+        is(channels.indexOf(result.channel), channels.length - 1, "Last channel used");
+        is(result.data, message, "Received message has the correct content.");
+      });
+    },
 
-        test.send(message, function (channel, data) {
-          is(channels.indexOf(channel), channels.length - 1, "Last channel used");
-          is(data, message, "Received message has the correct content.");
 
-          test.next();
-        });
-      }
-    ],
-    [
-      'SEND_MESSAGE_THROUGH_FIRST_CHANNEL',
-      function (test) {
-        var message = "Message through 1st channel";
-        var options = {
-          sourceChannel: test.pcLocal.dataChannels[0],
-          targetChannel: test.pcRemote.dataChannels[0]
-        };
+    function SEND_MESSAGE_THROUGH_FIRST_CHANNEL(test) {
+      var message = "Message through 1st channel";
+      var options = {
+        sourceChannel: test.pcLocal.dataChannels[0],
+        targetChannel: test.pcRemote.dataChannels[0]
+      };
 
-        test.send(message, function (channel, data) {
-          is(test.pcRemote.dataChannels.indexOf(channel), 0, "1st channel used");
-          is(data, message, "Received message has the correct content.");
+      return test.send(message, options).then(result => {
+        is(test.pcRemote.dataChannels.indexOf(result.channel), 0, "1st channel used");
+        is(result.data, message, "Received message has the correct content.");
+      });
+    },
+
 
-          test.next();
-        }, options);
-      }
-    ],
-    [
-      'SEND_MESSAGE_BACK_THROUGH_FIRST_CHANNEL',
-      function (test) {
-        var message = "Return a message also through 1st channel";
-        var options = {
-          sourceChannel: test.pcRemote.dataChannels[0],
-          targetChannel: test.pcLocal.dataChannels[0]
-        };
+    function SEND_MESSAGE_BACK_THROUGH_FIRST_CHANNEL(test) {
+      var message = "Return a message also through 1st channel";
+      var options = {
+        sourceChannel: test.pcRemote.dataChannels[0],
+        targetChannel: test.pcLocal.dataChannels[0]
+      };
 
-        test.send(message, function (channel, data) {
-          is(test.pcLocal.dataChannels.indexOf(channel), 0, "1st channel used");
-          is(data, message, "Return message has the correct content.");
+      return test.send(message, options).then(result => {
+        is(test.pcLocal.dataChannels.indexOf(result.channel), 0, "1st channel used");
+        is(result.data, message, "Return message has the correct content.");
+      });
+    },
 
-          test.next();
-        }, options);
-      }
-    ],
-    [
-      'CREATE_NEGOTIATED_DATA_CHANNEL',
-      function (test) {
-        var options = {negotiated:true, id: 5, protocol:"foo/bar", ordered:false,
-          maxRetransmits:500};
-        test.createDataChannel(options, function (sourceChannel2, targetChannel2) {
-          is(sourceChannel2.readyState, "open", sourceChannel2 + " is in state: 'open'");
-          is(targetChannel2.readyState, "open", targetChannel2 + " is in state: 'open'");
+    function CREATE_NEGOTIATED_DATA_CHANNEL(test) {
+      var options = {
+        negotiated:true,
+        id: 5,
+        protocol: "foo/bar",
+        ordered: false,
+        maxRetransmits: 500
+      };
+      return test.createDataChannel(options).then(result => {
+        var sourceChannel2 = result.local;
+        var targetChannel2 = result.remote;
+        is(sourceChannel2.readyState, "open", sourceChannel2 + " is in state: 'open'");
+        is(targetChannel2.readyState, "open", targetChannel2 + " is in state: 'open'");
 
-          is(targetChannel2.binaryType, "blob", targetChannel2 + " is of binary type 'blob'");
-          is(targetChannel2.readyState, "open", targetChannel2 + " is in state: 'open'");
+        is(targetChannel2.binaryType, "blob", targetChannel2 + " is of binary type 'blob'");
 
-          if (options.id != undefined) {
-            is(sourceChannel2.id, options.id, sourceChannel2 + " id is:" + sourceChannel2.id);
-          }
-          else {
-            options.id = sourceChannel2.id;
-          }
-          var reliable = !options.ordered ? false : (options.maxRetransmits || options.maxRetransmitTime);
-          is(sourceChannel2.protocol, options.protocol, sourceChannel2 + " protocol is:" + sourceChannel2.protocol);
-          is(sourceChannel2.reliable, reliable, sourceChannel2 + " reliable is:" + sourceChannel2.reliable);
-  /*
-    These aren't exposed by IDL yet
+        is(sourceChannel2.id, options.id, sourceChannel2 + " id is:" + sourceChannel2.id);
+        var reliable = !options.ordered ? false : (options.maxRetransmits || options.maxRetransmitTime);
+        is(sourceChannel2.protocol, options.protocol, sourceChannel2 + " protocol is:" + sourceChannel2.protocol);
+        is(sourceChannel2.reliable, reliable, sourceChannel2 + " reliable is:" + sourceChannel2.reliable);
+        /*
+          These aren't exposed by IDL yet
           is(sourceChannel2.ordered, options.ordered, sourceChannel2 + " ordered is:" + sourceChannel2.ordered);
           is(sourceChannel2.maxRetransmits, options.maxRetransmits, sourceChannel2 + " maxRetransmits is:" +
-       sourceChannel2.maxRetransmits);
+          sourceChannel2.maxRetransmits);
           is(sourceChannel2.maxRetransmitTime, options.maxRetransmitTime, sourceChannel2 + " maxRetransmitTime is:" +
-       sourceChannel2.maxRetransmitTime);
-  */
-
-          is(targetChannel2.id, options.id, targetChannel2 + " id is:" + targetChannel2.id);
-          is(targetChannel2.protocol, options.protocol, targetChannel2 + " protocol is:" + targetChannel2.protocol);
-          is(targetChannel2.reliable, reliable, targetChannel2 + " reliable is:" + targetChannel2.reliable);
-  /*
-    These aren't exposed by IDL yet
-         is(targetChannel2.ordered, options.ordered, targetChannel2 + " ordered is:" + targetChannel2.ordered);
-          is(targetChannel2.maxRetransmits, options.maxRetransmits, targetChannel2 + " maxRetransmits is:" +
-       targetChannel2.maxRetransmits);
-          is(targetChannel2.maxRetransmitTime, options.maxRetransmitTime, targetChannel2 + " maxRetransmitTime is:" +
-       targetChannel2.maxRetransmitTime);
-  */
+          sourceChannel2.maxRetransmitTime);
+        */
 
-          test.next();
-        });
-      }
-    ],
-    [
-      'SEND_MESSAGE_THROUGH_LAST_OPENED_CHANNEL2',
-      function (test) {
-        var channels = test.pcRemote.dataChannels;
-        var message = "Lorem ipsum dolor sit amet";
+        is(targetChannel2.id, options.id, targetChannel2 + " id is:" + targetChannel2.id);
+        is(targetChannel2.protocol, options.protocol, targetChannel2 + " protocol is:" + targetChannel2.protocol);
+        is(targetChannel2.reliable, reliable, targetChannel2 + " reliable is:" + targetChannel2.reliable);
+        /*
+          These aren't exposed by IDL yet
+          is(targetChannel2.ordered, options.ordered, targetChannel2 + " ordered is:" + targetChannel2.ordered);
+          is(targetChannel2.maxRetransmits, options.maxRetransmits, targetChannel2 + " maxRetransmits is:" +
+          targetChannel2.maxRetransmits);
+          is(targetChannel2.maxRetransmitTime, options.maxRetransmitTime, targetChannel2 + " maxRetransmitTime is:" +
+          targetChannel2.maxRetransmitTime);
+        */
+      });
+    },
 
-        test.send(message, function (channel, data) {
-          is(channels.indexOf(channel), channels.length - 1, "Last channel used");
-          is(data, message, "Received message has the correct content.");
+    function SEND_MESSAGE_THROUGH_LAST_OPENED_CHANNEL2(test) {
+      var channels = test.pcRemote.dataChannels;
+      var message = "I am the walrus; Goo goo g'joob";
 
-          test.next();
-        });
-      }
-    ]
+      return test.send(message).then(result => {
+        is(channels.indexOf(result.channel), channels.length - 1, "Last channel used");
+        is(result.data, message, "Received message has the correct content.");
+      });
+    }
   ]);
 }
--- a/dom/media/tests/mochitest/head.js
+++ b/dom/media/tests/mochitest/head.js
@@ -1,15 +1,16 @@
 /* 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/. */
 
+"use strict";
+
 var Cc = SpecialPowers.Cc;
 var Ci = SpecialPowers.Ci;
-var Cr = SpecialPowers.Cr;
 
 // Specifies whether we are using fake streams to run this automation
 var FAKE_ENABLED = true;
 try {
   var audioDevice = SpecialPowers.getCharPref('media.audio_loopback_dev');
   var videoDevice = SpecialPowers.getCharPref('media.video_loopback_dev');
   dump('TEST DEVICES: Using media devices:\n');
   dump('audio: ' + audioDevice + '\nvideo: ' + videoDevice + '\n');
@@ -27,37 +28,37 @@ try {
  *        Meta information of the test
  * @param {string} meta.title
  *        Description of the test
  * @param {string} [meta.bug]
  *        Bug the test was created for
  * @param {boolean} [meta.visible=false]
  *        Visibility of the media elements
  */
-function createHTML(meta) {
+function realCreateHTML(meta) {
   var test = document.getElementById('test');
 
   // Create the head content
   var elem = document.createElement('meta');
   elem.setAttribute('charset', 'utf-8');
   document.head.appendChild(elem);
 
   var title = document.createElement('title');
   title.textContent = meta.title;
   document.head.appendChild(title);
 
   // Create the body content
   var anchor = document.createElement('a');
-  anchor.setAttribute('target', '_blank');
-
+  anchor.textContent = meta.title;
   if (meta.bug) {
     anchor.setAttribute('href', 'https://bugzilla.mozilla.org/show_bug.cgi?id=' + meta.bug);
+  } else {
+    anchor.setAttribute('target', '_blank');
   }
 
-  anchor.textContent = meta.title;
   document.body.insertBefore(anchor, test);
 
   var display = document.createElement('p');
   display.setAttribute('id', 'display');
   document.body.insertBefore(display, test);
 
   var content = document.createElement('div');
   content.setAttribute('id', 'content');
@@ -76,110 +77,119 @@ function createHTML(meta) {
  *        Description to use for the element
  * @return {HTMLMediaElement} The created HTML media element
  */
 function createMediaElement(type, label) {
   var id = label + '_' + type;
   var element = document.getElementById(id);
 
   // Sanity check that we haven't created the element already
-  if (element)
+  if (element) {
     return element;
+  }
 
   element = document.createElement(type === 'audio' ? 'audio' : 'video');
   element.setAttribute('id', id);
   element.setAttribute('height', 100);
   element.setAttribute('width', 150);
   element.setAttribute('controls', 'controls');
+  element.setAttribute('autoplay', 'autoplay');
   document.getElementById('content').appendChild(element);
 
   return element;
 }
 
 
 /**
  * Wrapper function for mozGetUserMedia to allow a singular area of control
  * for determining whether we run this with fake devices or not.
  *
  * @param {Dictionary} constraints
  *        The constraints for this mozGetUserMedia callback
- * @param {Function} onSuccess
- *        The success callback if the stream is successfully retrieved
- * @param {Function} onError
- *        The error callback if the stream fails to be retrieved
  */
-function getUserMedia(constraints, onSuccess, onError) {
+function getUserMedia(constraints) {
   if (!("fake" in constraints) && FAKE_ENABLED) {
     constraints["fake"] = FAKE_ENABLED;
   }
 
   info("Call getUserMedia for " + JSON.stringify(constraints));
-  navigator.mozGetUserMedia(constraints, onSuccess, onError);
+  return navigator.mediaDevices.getUserMedia(constraints);
 }
 
+// These are the promises we use to track that the prerequisites for the test
+// are in place before running it.  Users of this file need to ensure that they
+// also provide a promise called `scriptsReady` as well.
+var setTestOptions;
+var testConfigured = new Promise(r => setTestOptions = r);
 
-/**
- * Setup any Mochitest for WebRTC by enabling the preference for
- * peer connections. As by bug 797979 it will also enable mozGetUserMedia()
- * and disable the mozGetUserMedia() permission checking.
- *
- * @param {Function} aCallback
- *        Test method to execute after initialization
- */
-function runTest(aCallback) {
-  if (window.SimpleTest) {
-    // Running as a Mochitest.
-    SimpleTest.waitForExplicitFinish();
-    SimpleTest.requestFlakyTimeout("WebRTC inherently depends on timeouts");
-    SpecialPowers.pushPrefEnv({'set': [
+function setupEnvironment() {
+  if (!window.SimpleTest) {
+    return Promise.resolve();
+  }
+
+  // Running as a Mochitest.
+  SimpleTest.requestFlakyTimeout("WebRTC inherently depends on timeouts");
+  window.finish = () => SimpleTest.finish();
+  SpecialPowers.pushPrefEnv({
+    'set': [
       ['dom.messageChannel.enabled', true],
       ['media.peerconnection.enabled', true],
       ['media.peerconnection.identity.enabled', true],
       ['media.peerconnection.identity.timeout', 12000],
       ['media.peerconnection.default_iceservers', '[]'],
       ['media.navigator.permission.disabled', true],
       ['media.getusermedia.screensharing.enabled', true],
-      ['media.getusermedia.screensharing.allowed_domains', "mochi.test"]]
-    }, function () {
-      try {
-        aCallback();
-      }
-      catch (err) {
-        generateErrorCallback()(err);
-      }
-    });
-  } else {
-    // Steeplechase, let it call the callback.
-    window.run_test = function(is_initiator) {
-      var options = {is_local: is_initiator,
-                     is_remote: !is_initiator};
-      aCallback(options);
-    };
-    // Also load the steeplechase test code.
-    var s = document.createElement("script");
-    s.src = "/test.js";
-    document.head.appendChild(s);
-  }
+      ['media.getusermedia.screensharing.allowed_domains', "mochi.test"]
+    ]
+  }, setTestOptions);
 }
 
+// This is called by steeplechase; which provides the test configuration options
+// directly to the test through this function.  If we're not on steeplechase,
+// the test is configured directly and immediately.
+function run_test(is_initiator) {
+  var options = { is_local: is_initiator,
+                  is_remote: !is_initiator };
+
+  // Also load the steeplechase test code.
+  var s = document.createElement("script");
+  s.src = "/test.js";
+  s.onload = () => setTestOptions(options);
+  document.head.appendChild(s);
+}
+
+function runTestWhenReady(testFunc) {
+  setupEnvironment();
+  return Promise.all([scriptsReady, testConfigured]).then(() => {
+    try {
+      return testConfigured.then(options => testFunc(options));
+    } catch (e) {
+      ok(false, 'Error executing test: ' + e +
+         ((typeof e.stack === 'string') ?
+          (' ' + e.stack.split('\n').join(' ... ')) : ''));
+    }
+  });
+}
+
+
 /**
  * Checks that the media stream tracks have the expected amount of tracks
  * with the correct kind and id based on the type and constraints given.
  *
  * @param {Object} constraints specifies whether the stream should have
  *                             audio, video, or both
  * @param {String} type the type of media stream tracks being checked
  * @param {sequence<MediaStreamTrack>} mediaStreamTracks the media stream
  *                                     tracks being checked
  */
 function checkMediaStreamTracksByType(constraints, type, mediaStreamTracks) {
-  if(constraints[type]) {
+  if (constraints[type]) {
     is(mediaStreamTracks.length, 1, 'One ' + type + ' track shall be present');
 
-    if(mediaStreamTracks.length) {
+    if (mediaStreamTracks.length) {
       is(mediaStreamTracks[0].kind, type, 'Track kind should be ' + type);
       ok(mediaStreamTracks[0].id, 'Track id should be defined');
     }
   } else {
     is(mediaStreamTracks.length, 0, 'No ' + type + ' tracks shall be present');
   }
 }
 
@@ -193,38 +203,36 @@ function checkMediaStreamTracksByType(co
  */
 function checkMediaStreamTracks(constraints, mediaStream) {
   checkMediaStreamTracksByType(constraints, 'audio',
     mediaStream.getAudioTracks());
   checkMediaStreamTracksByType(constraints, 'video',
     mediaStream.getVideoTracks());
 }
 
-/**
- * Utility methods
- */
+/*** Utility methods */
+
+/** The dreadful setTimeout, use sparingly */
+function wait(time) {
+  return new Promise(r => setTimeout(r, time));
+}
 
-/**
- * Returns the contents of a blob as text
- *
- * @param {Blob} blob
-          The blob to retrieve the contents from
- * @param {Function} onSuccess
-          Callback with the blobs content as parameter
- */
-function getBlobContent(blob, onSuccess) {
-  var reader = new FileReader();
+/** The even more dreadful setInterval, use even more sparingly */
+function waitUntil(func, time) {
+  return new Promise(resolve => {
+    var interval = setInterval(() => {
+      if (func())  {
+        clearInterval(interval);
+        resolve();
+      }
+    }, time || 200);
+  });
+}
 
-  // Listen for 'onloadend' which will always be called after a success or failure
-  reader.onloadend = function (event) {
-    onSuccess(event.target.result);
-  };
-
-  reader.readAsText(blob);
-}
+/*** Test control flow methods */
 
 /**
  * Generates a callback function fired only under unexpected circumstances
  * while running the tests. The generated function kills off the test as well
  * gracefully.
  *
  * @param {String} [message]
  *        An optional message to show if no object gets passed into the
@@ -233,60 +241,247 @@ function getBlobContent(blob, onSuccess)
 function generateErrorCallback(message) {
   var stack = new Error().stack.split("\n");
   stack.shift(); // Don't include this instantiation frame
 
   /**
    * @param {object} aObj
    *        The object fired back from the callback
    */
-  return function (aObj) {
+  return aObj => {
     if (aObj) {
       if (aObj.name && aObj.message) {
         ok(false, "Unexpected callback for '" + aObj.name +
            "' with message = '" + aObj.message + "' at " +
            JSON.stringify(stack));
       } else {
         ok(false, "Unexpected callback with = '" + aObj +
            "' at: " + JSON.stringify(stack));
       }
     } else {
       ok(false, "Unexpected callback with message = '" + message +
          "' at: " + JSON.stringify(stack));
     }
-    SimpleTest.finish();
+    throw new Error("Unexpected callback");
   }
 }
 
+var unexpectedEventArrived;
+var rejectOnUnexpectedEvent = new Promise((x, reject) => {
+  unexpectedEventArrived = reject;
+});
+
 /**
  * Generates a callback function fired only for unexpected events happening.
  *
  * @param {String} description
           Description of the object for which the event has been fired
  * @param {String} eventName
           Name of the unexpected event
  */
-function unexpectedEventAndFinish(message, eventName) {
+function unexpectedEvent(message, eventName) {
   var stack = new Error().stack.split("\n");
   stack.shift(); // Don't include this instantiation frame
 
-  return function () {
-    ok(false, "Unexpected event '" + eventName + "' fired with message = '" +
-       message + "' at: " + JSON.stringify(stack));
-    SimpleTest.finish();
+  return e => {
+    var details = "Unexpected event '" + eventName + "' fired with message = '" +
+        message + "' at: " + JSON.stringify(stack);
+    ok(false, details);
+    unexpectedEventArrived(new Error(details));
   }
 }
 
-function IsMacOSX10_6orOlder() {
-    var is106orOlder = false;
+/**
+ * Implements the one-shot event pattern used throughout.  Each of the 'onxxx'
+ * attributes on the wrappers can be set with a custom handler.  Prior to the
+ * handler being set, if the event fires, it causes the test execution to halt.
+ * That handler is used exactly once, after which the original, error-generating
+ * handler is re-installed.  Thus, each event handler is used at most once.
+ *
+ * @param {object} wrapper
+ *        The wrapper on which the psuedo-handler is installed
+ * @param {object} obj
+ *        The real source of events
+ * @param {string} event
+ *        The name of the event
+ */
+function createOneShotEventWrapper(wrapper, obj, event) {
+  var onx = 'on' + event;
+  var unexpected = unexpectedEvent(wrapper, event);
+  wrapper[onx] = unexpected;
+  obj[onx] = e => {
+    info(wrapper + ': "on' + event + '" event fired');
+    e.wrapper = wrapper;
+    wrapper[onx](e);
+    wrapper[onx] = unexpected;
+  };
+}
 
-    if (navigator.platform.indexOf("Mac") == 0) {
-        var version = Cc["@mozilla.org/system-info;1"]
-                        .getService(SpecialPowers.Ci.nsIPropertyBag2)
-                        .getProperty("version");
-        // the next line is correct: Mac OS 10.6 corresponds to Darwin version 10.x !
-        // Mac OS 10.7 is Darwin version 11.x. the |version| string we've got here
-        // is the Darwin version.
-        is106orOlder = (parseFloat(version) < 11.0);
-    }
-    return is106orOlder;
+
+/**
+ * This class executes a series of functions in a continuous sequence.
+ * Promise-bearing functions are executed after the previous promise completes.
+ *
+ * @constructor
+ * @param {object} framework
+ *        A back reference to the framework which makes use of the class. It is
+ *        passed to each command callback.
+ * @param {function[]} commandList
+ *        Commands to set during initialization
+ */
+function CommandChain(framework, commandList) {
+  this._framework = framework;
+  this.commands = commandList || [ ];
 }
 
+CommandChain.prototype = {
+  /**
+   * Start the command chain.  This returns a promise that always resolves
+   * cleanly (this catches errors and fails the test case).
+   */
+  execute: function () {
+    return this.commands.reduce((prev, next, i) => {
+      if (typeof next !== 'function' || !next.name) {
+        throw new Error('registered non-function' + next);
+      }
+
+      return prev.then(() => {
+        info('Run step ' + (i + 1) + ': ' + next.name);
+        return Promise.race([ next(this._framework), rejectOnUnexpectedEvent ]);
+      });
+    }, Promise.resolve())
+      .catch(e =>
+             ok(false, 'Error in test execution: ' + e +
+                ((typeof e.stack === 'string') ?
+                 (' ' + e.stack.split('\n').join(' ... ')) : '')));
+  },
+
+  /**
+   * Add new commands to the end of the chain
+   */
+  append: function(commands) {
+    this.commands = this.commands.concat(commands);
+  },
+
+  /**
+   * Returns the index of the specified command in the chain.
+   */
+  indexOf: function(functionOrName) {
+    if (typeof functionOrName === 'string') {
+      return this.commands.findIndex(f => f.name === functionOrName);
+    }
+    return this.commands.indexOf(functionOrName);
+  },
+
+  /**
+   * Inserts the new commands after the specified command.
+   */
+  insertAfter: function(functionOrName, commands) {
+    this._insertHelper(functionOrName, commands, 1);
+  },
+
+  /**
+   * Inserts the new commands before the specified command.
+   */
+  insertBefore: function(functionOrName, commands) {
+    this._insertHelper(functionOrName, commands, 0);
+  },
+
+  _insertHelper: function(functionOrName, commands, delta) {
+    var index = this.indexOf(functionOrName);
+
+    if (index >= 0) {
+      this.commands = [].concat(
+        this.commands.slice(0, index + delta),
+        commands,
+        this.commands.slice(index + delta));
+    }
+  },
+
+  /**
+   * Removes the specified command, returns what was removed.
+   */
+  remove: function(functionOrName) {
+    var index = this.indexOf(functionOrName);
+    if (index >= 0) {
+      return this.commands.splice(index, 1);
+    }
+    return [];
+  },
+
+  /**
+   * Removes all commands after the specified one, returns what was removed.
+   */
+  removeAfter: function(functionOrName) {
+    var index = this.indexOf(functionOrName);
+    if (index >= 0) {
+      return this.commands.splice(index + 1);
+    }
+    return [];
+  },
+
+  /**
+   * Removes all commands before the specified one, returns what was removed.
+   */
+  removeBefore: function(functionOrName) {
+    var index = this.indexOf(functionOrName);
+    if (index >= 0) {
+      return this.commands.splice(0, index);
+    }
+    return [];
+  },
+
+  /**
+   * Replaces a single command, returns what was removed.
+   */
+  replace: function(functionOrName, commands) {
+    this.insertBefore(functionOrName, commands);
+    return this.remove(functionOrName);
+  },
+
+  /**
+   * Replaces all commands after the specified one, returns what was removed.
+   */
+  replaceAfter: function(functionOrName, commands) {
+    var oldCommands = this.removeAfter(functionOrName);
+    this.append(commands);
+    return oldCommands;
+  },
+
+  /**
+   * Replaces all commands before the specified one, returns what was removed.
+   */
+  replaceBefore: function(functionOrName, commands) {
+    var oldCommands = this.removeBefore(functionOrName);
+    this.insertBefore(functionOrName, commands);
+    return oldCommands;
+  },
+
+  /**
+   * Remove all commands whose name match the specified regex.
+   */
+  filterOut: function (id_match) {
+    this.commands = this.commands.filter(c => !id_match.test(c.name));
+  }
+};
+
+
+function IsMacOSX10_6orOlder() {
+  if (navigator.platform.indexOf("Mac") !== 0) {
+    return false;
+  }
+
+  var version = Cc["@mozilla.org/system-info;1"]
+      .getService(Ci.nsIPropertyBag2)
+      .getProperty("version");
+  // the next line is correct: Mac OS 10.6 corresponds to Darwin version 10.x !
+  // Mac OS 10.7 is Darwin version 11.x. the |version| string we've got here
+  // is the Darwin version.
+  return (parseFloat(version) < 11.0);
+}
+
+(function(){
+  var el = document.createElement("link");
+  el.rel = "stylesheet";
+  el.type = "text/css";
+  el.href= "/tests/SimpleTest/test.css";
+  document.head.appendChild(el);
+}());
--- a/dom/media/tests/mochitest/identity/identityevent.js
+++ b/dom/media/tests/mochitest/identity/identityevent.js
@@ -1,12 +1,12 @@
 (function(g) {
   'use strict';
 
-  g.trapIdentityEvents = function(target) {
+  g.trapIdentityEvents = target => {
     var state = {};
     var identityEvents = ['idpassertionerror', 'idpvalidationerror',
                           'identityresult', 'peeridentity'];
     identityEvents.forEach(function(name) {
       target.addEventListener(name, function(e) {
         state[name] = e;
       }, false);
     });
--- a/dom/media/tests/mochitest/identity/test_getIdentityAssertion.html
+++ b/dom/media/tests/mochitest/identity/test_getIdentityAssertion.html
@@ -1,117 +1,110 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="../head.js"></script>
+  <script type="application/javascript">var scriptRelativePath = "../";</script>
   <script type="application/javascript" src="../pc.js"></script>
-  <script type="application/javascript" src="../templates.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
-    title: "getIdentityAssertion Tests"
+    title: "getIdentityAssertion Tests",
+    bug: "942367"
   });
 
 function checkIdentity(assertion, identity) {
   // here we dig into the payload, which means we need to know something
   // about how the IdP actually works (not good in general, but OK here)
   var assertion = JSON.parse(atob(assertion)).assertion;
   var user = JSON.parse(assertion).username;
   is(user, identity, "id should be '" + identity + "' is '" + user + "'");
 }
 
 var test;
 function theTest() {
   test = new PeerConnectionTest();
   test.setMediaConstraints([{audio: true}], [{audio: true}]);
   test.chain.removeAfter('PC_REMOTE_CHECK_INITIAL_SIGNALINGSTATE');
   test.chain.append([
-  [
-    "GET_IDENTITY_ASSERTION_FAILS_WITHOUT_PROVIDER",
-    function(test) {
-      test.pcLocal._pc.onidpassertionerror = function(e) {
-        ok(e, "getIdentityAssertion must fail without provider");
-        test.next();
-      };
-      test.pcLocal._pc.getIdentityAssertion();
+    function GET_IDENTITY_ASSERTION_FAILS_WITHOUT_PROVIDER(test) {
+      return new Promise(resolve => {
+        test.pcLocal._pc.onidpassertionerror = function(e) {
+          ok(e, "getIdentityAssertion must fail without provider");
+          resolve();
+        };
+        test.pcLocal._pc.getIdentityAssertion();
+      });
+    },
+    function GET_IDENTITY_ASSERTION_FIRES_EVENTUALLY_AND_SUBSEQUENTLY(test) {
+      return new Promise(resolve => {
+        var fired = 0;
+        test.setIdentityProvider(test.pcLocal, 'example.com', 'idp.html');
+        test.pcLocal._pc.onidentityresult = function(e) {
+          fired++;
+          if (fired == 1) {
+            ok(true, "identityresult fired");
+            checkIdentity(e.assertion, 'someone@example.com');
+          } else if (fired == 2) {
+            ok(true, "identityresult fired 2x");
+            checkIdentity(e.assertion, 'someone@example.com');
+            resolve();
+          }
+        };
+        test.pcLocal._pc.onidpassertionerror = function(e) {
+          ok(false, "error event fired");
+          resolve();
+        };
+        test.pcLocal._pc.getIdentityAssertion();
+        test.pcLocal._pc.getIdentityAssertion();
+      });
     },
-  ],
-  [
-    "GET_IDENTITY_ASSERTION_FIRES_EVENTUALLY_AND_SUBSEQUENTLY",
-    function(test) {
-      var fired = 0;
-      test.setIdentityProvider(test.pcLocal, 'example.com', 'idp.html');
-      test.pcLocal._pc.onidentityresult = function(e) {
-        fired++;
-        if (fired == 1) {
-          ok(true, "identityresult fired");
-          checkIdentity(e.assertion, 'someone@example.com');
-        } else if (fired == 2) {
-          ok(true, "identityresult fired 2x");
-          checkIdentity(e.assertion, 'someone@example.com');
-          test.next();
-        }
-      };
-      test.pcLocal._pc.onidpassertionerror = function(e) {
-        ok(false, "error event fired");
-        test.next();
-      };
-      test.pcLocal._pc.getIdentityAssertion();
-      test.pcLocal._pc.getIdentityAssertion();
+    function GET_IDENTITY_ASSERTION_FAILS(test) {
+      return new Promise(resolve => {
+        test.setIdentityProvider(test.pcLocal, 'example.com', 'idp.html#error');
+        test.pcLocal._pc.onidentityresult = function(e) {
+          ok(false, "Should not get an identity result");
+          resolve();
+        };
+        test.pcLocal._pc.onidpassertionerror = function(err) {
+          ok(err, "Got error event from getIdentityAssertion");
+          resolve();
+        };
+        test.pcLocal._pc.getIdentityAssertion();
+      });
+    },
+    function GET_IDENTITY_ASSERTION_IDP_NOT_READY(test) {
+      return new Promise(resolve => {
+        test.setIdentityProvider(test.pcLocal, 'example.com', 'idp.html#error:ready');
+        test.pcLocal._pc.onidentityresult = function(e) {
+          ok(false, "Should not get an identity result");
+          resolve();
+        };
+        test.pcLocal._pc.onidpassertionerror = function(e) {
+          ok(e, "Got error callback from getIdentityAssertion");
+          resolve();
+        };
+        test.pcLocal._pc.getIdentityAssertion();
+      });
+    },
+    function GET_IDENTITY_ASSERTION_WITH_SPECIFIC_NAME(test) {
+      return new Promise(resolve => {
+        test.setIdentityProvider(test.pcLocal, 'example.com', 'idp.html', 'user@example.com');
+        test.pcLocal._pc.onidentityresult = function(e) {
+          checkIdentity(e.assertion, 'user@example.com');
+          resolve();
+        };
+        test.pcLocal._pc.onidpassertionerror = function(e) {
+          ok(false, "Got error callback from getIdentityAssertion");
+          resolve();
+        };
+        test.pcLocal._pc.getIdentityAssertion();
+      });
     }
-  ],
-  [
-    "GET_IDENTITY_ASSERTION_FAILS",
-    function(test) {
-      test.setIdentityProvider(test.pcLocal, 'example.com', 'idp.html#error');
-      test.pcLocal._pc.onidentityresult = function(e) {
-        ok(false, "Should not get an identity result");
-        test.next();
-      };
-      test.pcLocal._pc.onidpassertionerror = function(err) {
-        ok(err, "Got error event from getIdentityAssertion");
-        test.next();
-      };
-      test.pcLocal._pc.getIdentityAssertion();
-    }
-  ],
-  [
-    "GET_IDENTITY_ASSERTION_IDP_NOT_READY",
-    function(test) {
-      test.setIdentityProvider(test.pcLocal, 'example.com', 'idp.html#error:ready');
-      test.pcLocal._pc.onidentityresult = function(e) {
-        ok(false, "Should not get an identity result");
-        test.next();
-      };
-      test.pcLocal._pc.onidpassertionerror = function(e) {
-        ok(e, "Got error callback from getIdentityAssertion");
-        test.next();
-      };
-      test.pcLocal._pc.getIdentityAssertion();
-    }
-  ],
-  [
-    "GET_IDENTITY_ASSERTION_WITH_SPECIFIC_NAME",
-    function(test) {
-      test.setIdentityProvider(test.pcLocal, 'example.com', 'idp.html', 'user@example.com');
-      test.pcLocal._pc.onidentityresult = function(e) {
-        checkIdentity(e.assertion, 'user@example.com');
-        test.next();
-      };
-      test.pcLocal._pc.onidpassertionerror = function(e) {
-        ok(false, "Got error callback from getIdentityAssertion");
-        test.next();
-      };
-      test.pcLocal._pc.getIdentityAssertion();
-    }
-  ]
   ]);
   test.run();
 }
 runNetworkTest(theTest);
 
 </script>
 </pre>
 </body>
--- a/dom/media/tests/mochitest/identity/test_peerConnection_peerIdentity.html
+++ b/dom/media/tests/mochitest/identity/test_peerConnection_peerIdentity.html
@@ -1,26 +1,21 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <meta charset="utf-8"/>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="../head.js"></script>
+  <script type="application/javascript">var scriptRelativePath = "../";</script>
   <script type="application/javascript" src="../pc.js"></script>
-  <script type="application/javascript" src="../templates.js"></script>
   <script type="application/javascript" src="../blacksilence.js"></script>
-  <script type="application/javascript" src="../turnConfig.js"></script>
 </head>
 <body>
-<div id="display"></div>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
-    title: "setIdentityProvider leads to peerIdentity and assertions in SDP"
+    title: "setIdentityProvider leads to peerIdentity and assertions in SDP",
+    bug: "942367"
   });
 
 var test;
 function theTest() {
   var id1 = 'someone@test1.example.com';
   var id2 = 'someone@test2.example.com';
   test = new PeerConnectionTest({
     config_local: {
@@ -38,48 +33,36 @@ function theTest() {
     audio: true,
     video: true,
     fake: true,
     peerIdentity: id1
   }]);
   test.setIdentityProvider(test.pcLocal, 'test1.example.com', 'idp.html');
   test.setIdentityProvider(test.pcRemote, 'test2.example.com', 'idp.html');
   test.chain.append([
-  [
-    "PEER_IDENTITY_IS_SET_CORRECTLY",
-    function(test) {
+
+    function PEER_IDENTITY_IS_SET_CORRECTLY(test) {
       // no need to wait to check identity in this case,
       // setRemoteDescription should wait for the IdP to complete
       function checkIdentity(pc, pfx, idp, name) {
         is(pc.peerIdentity.idp, idp, pfx + "IdP is correct");
         is(pc.peerIdentity.name, name + "@" + idp, pfx + "identity is correct");
       }
 
       checkIdentity(test.pcLocal._pc, "local: ", "test2.example.com", "someone");
       checkIdentity(test.pcRemote._pc, "remote: ", "test1.example.com", "someone");
-      test.next();
-    }
-  ],
-  [
-    "REMOTE_STREAMS_ARE_RESTRICTED",
-    function(test) {
+    },
+
+    function REMOTE_STREAMS_ARE_RESTRICTED(test) {
       var remoteStream = test.pcLocal._pc.getRemoteStreams()[0];
-      var oneDone = false;
-      function done() {
-        if (!oneDone) {
-          oneDone = true;
-          return;
-        }
-        test.next();
-      }
-
-      audioIsSilence(true, remoteStream, done);
-      videoIsBlack(true, remoteStream, done);
+      return Promise.all([
+        new Promise(done => audioIsSilence(true, remoteStream, done)),
+        new Promise(done => videoIsBlack(true, remoteStream, done))
+      ]);
     }
-  ],
   ]);
   test.run();
 }
 runNetworkTest(theTest);
 
 </script>
 </pre>
 </body>
--- a/dom/media/tests/mochitest/identity/test_setIdentityProvider.html
+++ b/dom/media/tests/mochitest/identity/test_setIdentityProvider.html
@@ -1,115 +1,95 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <meta charset="utf-8"/>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="../head.js"></script>
+  <script type="application/javascript">var scriptRelativePath = "../";</script>
   <script type="application/javascript" src="../pc.js"></script>
-  <script type="application/javascript" src="../templates.js"></script>
   <script type="application/javascript" src="identityevent.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
-    title: "setIdentityProvider leads to peerIdentity and assertions in SDP"
+    title: "setIdentityProvider leads to peerIdentity and assertions in SDP",
+    bug: "942367"
   });
 
 var test;
 function theTest() {
   test = new PeerConnectionTest();
   test.setMediaConstraints([{audio: true}], [{audio: true}]);
   test.setIdentityProvider(test.pcLocal, "test1.example.com", "idp.html", "someone");
   test.setIdentityProvider(test.pcRemote, "test2.example.com", "idp.html", "someone");
 
   var localEvents = trapIdentityEvents(test.pcLocal._pc);
   var remoteEvents = trapIdentityEvents(test.pcRemote._pc);
 
   test.chain.append([
-  [
-    "PEER_IDENTITY_IS_SET_CORRECTLY",
-    function(test) {
+    function PEER_IDENTITY_IS_SET_CORRECTLY(test) {
       var outstanding = 0;
       // we have to wait for the identity result in order to get the actual
       // identity information, since the call will complete before the identity
       // provider has a chance to finish verifying... that's OK, but it makes
       // testing more difficult
 
-      function checkOrSetupCheck(pc, pfx, idp, name) {
+      function checkOrSetupCheck(pc, prefix, idp, name) {
         function checkIdentity() {
-          ok(pc.peerIdentity, pfx + "peerIdentity is set");
-          is(pc.peerIdentity.idp, idp, pfx + "IdP is correct");
-          is(pc.peerIdentity.name, name + "@" + idp, pfx + "identity is correct");
+          ok(pc.peerIdentity, prefix + "peerIdentity is set");
+          is(pc.peerIdentity.idp, idp, prefix + "IdP is correct");
+          is(pc.peerIdentity.name, name + "@" + idp, prefix + "identity is correct");
         }
         if (pc.peerIdentity) {
-          info(pfx + "peerIdentity already set");
+          info(prefix + "peerIdentity already set");
           checkIdentity();
-        } else {
-          ++outstanding;
-          info(pfx + "setting onpeeridentity handler");
-          pc.onpeeridentity = function checkIdentityEvent(e) {
-            info(pfx + "checking peerIdentity");
+          return Promise.resolve();
+        }
+
+        return new Promise(resolve => {
+          info(prefix + "setting onpeeridentity handler");
+          pc.onpeeridentity = e => {
             checkIdentity();
-            --outstanding;
-            if (outstanding <= 0) {
-              test.next();
-            }
+            resolve();
           };
-        }
+        });
       }
 
-      checkOrSetupCheck(test.pcLocal._pc, "local: ", "test2.example.com", "someone");
-      checkOrSetupCheck(test.pcRemote._pc, "remote: ", "test1.example.com", "someone");
-      if (outstanding <= 0) {
-        test.next();
-      }
-    }
-  ],
-  [
-    "CHECK_IDENTITY_EVENTS",
-    function(test) {
+      return Promise.all([
+        checkOrSetupCheck(test.pcLocal._pc, "local: ", "test2.example.com", "someone"),
+        checkOrSetupCheck(test.pcRemote._pc, "remote: ", "test1.example.com", "someone")
+      ]);
+    },
+
+    function CHECK_IDENTITY_EVENTS(test) {
       ok(!localEvents.idpassertionerror , "No assertion generation errors on local");
       ok(!remoteEvents.idpassertionerror, "No assertion generation errors on remote");
       ok(!localEvents.idpvalidationerror, "No assertion validation errors on local");
-      ok( !remoteEvents.idpvalidationerror, "No assertion validation errors on remote");
+      ok(!remoteEvents.idpvalidationerror, "No assertion validation errors on remote");
       ok(localEvents.identityresult, "local acquired identity assertions");
       ok(remoteEvents.identityresult, "remote acquired identity assertions");
       ok(localEvents.peeridentity, "local got peer identity");
       ok(remoteEvents.peeridentity, "remote got peer identity");
-      test.next();
-    }
-  ],
-  [
-    "OFFERS_AND_ANSWERS_INCLUDE_IDENTITY",
-    function(test) {
+    },
+
+    function OFFERS_AND_ANSWERS_INCLUDE_IDENTITY(test) {
       ok(test.originalOffer.sdp.contains("a=identity"), "a=identity is in the offer SDP");
       ok(test.originalAnswer.sdp.contains("a=identity"), "a=identity is in the answer SDP");
-      test.next();
-    }
-  ],
-  [
-    "DESCRIPTIONS_CONTAIN_IDENTITY",
-    function(test) {
+    },
+
+    function DESCRIPTIONS_CONTAIN_IDENTITY(test) {
       ok(test.pcLocal.localDescription.sdp.contains("a=identity"),
-                         "a=identity is in the local copy of the offer");
+         "a=identity is in the local copy of the offer");
       ok(test.pcRemote.localDescription.sdp.contains("a=identity"),
-                         "a=identity is in the remote copy of the offer");
+         "a=identity is in the remote copy of the offer");
       ok(test.pcLocal.remoteDescription.sdp.contains("a=identity"),
-                         "a=identity is in the local copy of the answer");
+         "a=identity is in the local copy of the answer");
       ok(test.pcRemote.remoteDescription.sdp.contains("a=identity"),
-                         "a=identity is in the remote copy of the answer");
-      test.next();
+         "a=identity is in the remote copy of the answer");
     }
-  ]
   ]);
   test.run();
 }
 runNetworkTest(theTest);
 
-
-
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/identity/test_setIdentityProviderWithErrors.html
+++ b/dom/media/tests/mochitest/identity/test_setIdentityProviderWithErrors.html
@@ -1,85 +1,74 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="../head.js"></script>
+  <script type="application/javascript">var scriptRelativePath = "../";</script>
   <script type="application/javascript" src="../pc.js"></script>
-  <script type="application/javascript" src="../templates.js"></script>
   <script type="application/javascript" src="identityevent.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
-    title: "Identity Provider returning errors is handled correctly"
+    title: "Identity Provider returning errors is handled correctly",
+    bug: "942367"
   });
 
-var test;
 runNetworkTest(function () {
-  test = new PeerConnectionTest();
+  var test = new PeerConnectionTest();
   test.setMediaConstraints([{audio: true}], [{audio: true}]);
   // first example generates an error
   test.setIdentityProvider(test.pcLocal, 'example.com', 'idp.html#error', 'nobody');
   // second generates a bad assertion; which fails to validate
   test.setIdentityProvider(test.pcRemote, 'example.com', 'idp.html#bad', 'nobody');
 
   var localEvents = trapIdentityEvents(test.pcLocal._pc);
   var remoteEvents = trapIdentityEvents(test.pcRemote._pc);
 
   test.chain.append([
-    [
-      'CHECK_IDENTITY_EVENTS',
-      function(test) {
-        function checkEvents() {
-          ok(localEvents.idpassertionerror, 'local assertion generation should fail (idpassertionerror)');
-          is(localEvents.idpassertionerror.idp, 'example.com', 'event IdP is correct');
-          is(localEvents.idpassertionerror.protocol, 'idp.html#error', 'event IdP protocol is #error');
-          ok(!remoteEvents.idpassertionerror, 'remote assertion generation should succeed (idpassertionerror)');
-          ok(!localEvents.identityresult, 'local assertion generation should fail (identityresult)');
-          ok(remoteEvents.identityresult, 'remote assertion generation should succeed (identityresult)');
+    function CHECK_IDENTITY_EVENTS(test) {
+      function checkEvents() {
+        ok(localEvents.idpassertionerror, 'local assertion generation should fail (idpassertionerror)');
+        is(localEvents.idpassertionerror.idp, 'example.com', 'event IdP is correct');
+        is(localEvents.idpassertionerror.protocol, 'idp.html#error', 'event IdP protocol is #error');
+        ok(!remoteEvents.idpassertionerror, 'remote assertion generation should succeed (idpassertionerror)');
+        ok(!localEvents.identityresult, 'local assertion generation should fail (identityresult)');
+        ok(remoteEvents.identityresult, 'remote assertion generation should succeed (identityresult)');
 
-          ok(!localEvents.peeridentity, 'no peer identity event for local peer');
-          ok(!remoteEvents.peeridentity, 'no peer identity event for remote peer');
-          ok(localEvents.idpvalidationerror, 'local fails to validate');
-          is(localEvents.idpvalidationerror.idp, 'example.com', 'event IdP is correct');
-          is(localEvents.idpvalidationerror.protocol, 'idp.html#bad', 'event IdP protocol is #bad');
-          ok(!remoteEvents.idpvalidationerror, 'remote doesn\'t even see an assertion');
+        ok(!localEvents.peeridentity, 'no peer identity event for local peer');
+        ok(!remoteEvents.peeridentity, 'no peer identity event for remote peer');
+        ok(localEvents.idpvalidationerror, 'local fails to validate');
+        is(localEvents.idpvalidationerror.idp, 'example.com', 'event IdP is correct');
+        is(localEvents.idpvalidationerror.protocol, 'idp.html#bad', 'event IdP protocol is #bad');
+        ok(!remoteEvents.idpvalidationerror, 'remote doesn\'t even see an assertion');
 
-          test.next();
-        }
+      }
 
-        // we actually have to wait on this because IdP validation happens asynchronously
-        if (localEvents.idpvalidationerror) {
-          checkEvents();
-        } else {
-          // have to let the other event handler have a chance to record success
-          // before we run the checks that rely on that recording
-          test.pcLocal._pc.onidpvalidationerror = setTimeout.bind(window, checkEvents, 1);
-        }
+      // we actually have to wait on this because IdP validation happens asynchronously
+      if (localEvents.idpvalidationerror) {
+        checkEvents();
+        return Promise.resolve();
       }
-    ],
-    [
-      'PEER_IDENTITY_IS_EMPTY',
-      function(test) {
-        ok(!test.pcLocal._pc.peerIdentity, 'local peerIdentity is not set');
-        ok(!test.pcRemote._pc.peerIdentity, 'remote peerIdentity is not set');
-        test.next();
-      }
-    ],
-    [
-      'ONLY_REMOTE_SDP_INCLUDES_IDENTITY_ASSERTION',
-      function(test) {
-        ok(!test.originalOffer.sdp.contains('a=identity'), 'a=identity not contained in the offer SDP');
-        ok(test.originalAnswer.sdp.contains('a=identity'), 'a=identity is contained in the answer SDP');
-        test.next();
-      }
-    ]
+      // have to let the other event handler have a chance to record success
+      // before we run the checks that rely on that recording
+      return new Promise(resolve => {
+        test.pcLocal._pc.onidpvalidationerror = resolve;
+      }).then(checkEvents);
+    },
+
+    function PEER_IDENTITY_IS_EMPTY(test) {
+      ok(!test.pcLocal._pc.peerIdentity, 'local peerIdentity is not set');
+      ok(!test.pcRemote._pc.peerIdentity, 'remote peerIdentity is not set');
+    },
+
+    function ONLY_REMOTE_SDP_INCLUDES_IDENTITY_ASSERTION(test) {
+      ok(!test.originalOffer.sdp.contains('a=identity'), 'a=identity not contained in the offer SDP');
+      ok(test.originalAnswer.sdp.contains('a=identity'), 'a=identity is contained in the answer SDP');
+    }
   ]);
   test.run();
 });
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/mediaStreamPlayback.js
+++ b/dom/media/tests/mochitest/mediaStreamPlayback.js
@@ -32,137 +32,123 @@ MediaStreamPlayback.prototype = {
    *
    * @param {Boolean} isResume specifies if this media element is being resumed
    *                           from a previous run
    * @param {Function} onSuccess the success callback if the media playback
    *                             start and stop cycle completes successfully
    * @param {Function} onError the error callback if the media playback
    *                           start and stop cycle fails
    */
-  playMedia : function MSP_playMedia(isResume, onSuccess, onError) {
-    var self = this;
-
-    this.startMedia(isResume, function() {
-      self.stopMediaElement();
-      onSuccess();
-    }, onError);
+  playMedia : function(isResume) {
+    return this.startMedia(isResume)
+      .then(() => this.stopMediaElement());
   },
 
   /**
    * Starts the media with the associated stream.
    *
    * @param {Boolean} isResume specifies if the media element playback
    *                           is being resumed from a previous run
-   * @param {Function} onSuccess the success function call back
-   *                             if media starts correctly
-   * @param {Function} onError the error function call back
-   *                           if media fails to start
    */
-  startMedia : function MSP_startMedia(isResume, onSuccess, onError) {
-    var self = this;
+  startMedia : function(isResume) {
     var canPlayThroughFired = false;
 
     // If we're initially running this media, check that the time is zero
     if (!isResume) {
       is(this.mediaStream.currentTime, 0,
          "Before starting the media element, currentTime = 0");
     }
 
-    /**
-     * Callback fired when the canplaythrough event is fired. We only
-     * run the logic of this function once, as this event can fire
-     * multiple times while a HTMLMediaStream is playing content from
-     * a real-time MediaStream.
-     */
-    var canPlayThroughCallback = function() {
-      // Disable the canplaythrough event listener to prevent multiple calls
-      canPlayThroughFired = true;
-      self.mediaElement.removeEventListener('canplaythrough',
-        canPlayThroughCallback, false);
+    return new Promise((resolve, reject) => {
+      /**
+       * Callback fired when the canplaythrough event is fired. We only
+       * run the logic of this function once, as this event can fire
+       * multiple times while a HTMLMediaStream is playing content from
+       * a real-time MediaStream.
+       */
+      var canPlayThroughCallback = () => {
+        // Disable the canplaythrough event listener to prevent multiple calls
+        canPlayThroughFired = true;
+        this.mediaElement.removeEventListener('canplaythrough',
+                                              canPlayThroughCallback, false);
 
-      is(self.mediaElement.paused, false,
-        "Media element should be playing");
-      is(self.mediaElement.duration, Number.POSITIVE_INFINITY,
-        "Duration should be infinity");
+        is(this.mediaElement.paused, false,
+           "Media element should be playing");
+        is(this.mediaElement.duration, Number.POSITIVE_INFINITY,
+           "Duration should be infinity");
 
-      // When the media element is playing with a real-time stream, we
-      // constantly switch between having data to play vs. queuing up data,
-      // so we can only check that the ready state is one of those two values
-      ok(self.mediaElement.readyState === HTMLMediaElement.HAVE_ENOUGH_DATA ||
-         self.mediaElement.readyState === HTMLMediaElement.HAVE_CURRENT_DATA,
-         "Ready state shall be HAVE_ENOUGH_DATA or HAVE_CURRENT_DATA");
+        // When the media element is playing with a real-time stream, we
+        // constantly switch between having data to play vs. queuing up data,
+        // so we can only check that the ready state is one of those two values
+        ok(this.mediaElement.readyState === HTMLMediaElement.HAVE_ENOUGH_DATA ||
+           this.mediaElement.readyState === HTMLMediaElement.HAVE_CURRENT_DATA,
+           "Ready state shall be HAVE_ENOUGH_DATA or HAVE_CURRENT_DATA");
+
+        is(this.mediaElement.seekable.length, 0,
+           "Seekable length shall be zero");
+        is(this.mediaElement.buffered.length, 0,
+           "Buffered length shall be zero");
 
-      is(self.mediaElement.seekable.length, 0,
-         "Seekable length shall be zero");
-      is(self.mediaElement.buffered.length, 0,
-         "Buffered length shall be zero");
+        is(this.mediaElement.seeking, false,
+           "MediaElement is not seekable with MediaStream");
+        ok(isNaN(this.mediaElement.startOffsetTime),
+           "Start offset time shall not be a number");
+        is(this.mediaElement.loop, false, "Loop shall be false");
+        is(this.mediaElement.preload, "", "Preload should not exist");
+        is(this.mediaElement.src, "", "No src should be defined");
+        is(this.mediaElement.currentSrc, "",
+           "Current src should still be an empty string");
 
-      is(self.mediaElement.seeking, false,
-         "MediaElement is not seekable with MediaStream");
-      ok(isNaN(self.mediaElement.startOffsetTime),
-         "Start offset time shall not be a number");
-      is(self.mediaElement.loop, false, "Loop shall be false");
-      is(self.mediaElement.preload, "", "Preload should not exist");
-      is(self.mediaElement.src, "", "No src should be defined");
-      is(self.mediaElement.currentSrc, "",
-         "Current src should still be an empty string");
+        var timeUpdateCallback = () => {
+          if (this.mediaStream.currentTime > 0 &&
+              this.mediaElement.currentTime > 0) {
+            this.mediaElement.removeEventListener('timeupdate',
+                                                  timeUpdateCallback, false);
+            resolve();
+          }
+        };
 
-      var timeUpdateFired = false;
+        // When timeupdate fires, we validate time has passed and move
+        // onto the success condition
+        this.mediaElement.addEventListener('timeupdate', timeUpdateCallback,
+                                           false);
 
-      var timeUpdateCallback = function() {
-        if (self.mediaStream.currentTime > 0 &&
-            self.mediaElement.currentTime > 0) {
-          timeUpdateFired = true;
-          self.mediaElement.removeEventListener('timeupdate',
-            timeUpdateCallback, false);
-          onSuccess();
-        }
+        // If timeupdate doesn't fire in enough time, we fail the test
+        setTimeout(() => {
+          this.mediaElement.removeEventListener('timeupdate',
+                                                timeUpdateCallback, false);
+          reject(new Error("timeUpdate event never fired"));
+        }, TIMEUPDATE_TIMEOUT_LENGTH);
       };
 
-      // When timeupdate fires, we validate time has passed and move
-      // onto the success condition
-      self.mediaElement.addEventListener('timeupdate', timeUpdateCallback,
-        false);
-
-      // If timeupdate doesn't fire in enough time, we fail the test
-      setTimeout(function() {
-        if (!timeUpdateFired) {
-          self.mediaElement.removeEventListener('timeupdate',
-            timeUpdateCallback, false);
-          onError("timeUpdate event never fired");
-        }
-      }, TIMEUPDATE_TIMEOUT_LENGTH);
-    };
+      // Adds a listener intended to be fired when playback is available
+      // without further buffering.
+      this.mediaElement.addEventListener('canplaythrough', canPlayThroughCallback,
+                                         false);
 
-    // Adds a listener intended to be fired when playback is available
-    // without further buffering.
-    this.mediaElement.addEventListener('canplaythrough', canPlayThroughCallback,
-      false);
-
-    // Hooks up the media stream to the media element and starts playing it
-    this.mediaElement.mozSrcObject = this.mediaStream;
-    this.mediaElement.play();
+      // Hooks up the media stream to the media element and starts playing it
+      this.mediaElement.mozSrcObject = this.mediaStream;
+      this.mediaElement.play();
 
-    // If canplaythrough doesn't fire in enough time, we fail the test
-    setTimeout(function() {
-      if (!canPlayThroughFired) {
-        self.mediaElement.removeEventListener('canplaythrough',
-          canPlayThroughCallback, false);
-        onError("canplaythrough event never fired");
-      }
-    }, CANPLAYTHROUGH_TIMEOUT_LENGTH);
+      // If canplaythrough doesn't fire in enough time, we fail the test
+      setTimeout(() => {
+        this.mediaElement.removeEventListener('canplaythrough',
+                                              canPlayThroughCallback, false);
+        reject(new Error("canplaythrough event never fired"));
+      }, CANPLAYTHROUGH_TIMEOUT_LENGTH);
+    });
   },
 
   /**
    * Stops the media with the associated stream.
    *
    * Precondition: The media stream and element should both be actively
    *               being played.
    */
-  stopMediaElement : function MSP_stopMediaElement() {
+  stopMediaElement : function() {
     this.mediaElement.pause();
     this.mediaElement.mozSrcObject = null;
   }
 }
 
 
 /**
  * This class is basically the same as MediaStreamPlayback except
@@ -181,69 +167,70 @@ function LocalMediaStreamPlayback(mediaE
 LocalMediaStreamPlayback.prototype = Object.create(MediaStreamPlayback.prototype, {
 
   /**
    * Starts media with a media stream, runs it until a canplaythrough and
    * timeupdate event fires, and calls stop() on the stream.
    *
    * @param {Boolean} isResume specifies if this media element is being resumed
    *                           from a previous run
-   * @param {Function} onSuccess the success callback if the media element
-   *                             successfully fires ended on a stop() call
-   *                             on the stream
-   * @param {Function} onError the error callback if the media element fails
-   *                           to fire an ended callback on a stop() call
-   *                           on the stream
    */
   playMediaWithStreamStop : {
-    value: function (isResume, onSuccess, onError) {
-      var self = this;
-
-      this.startMedia(isResume, function() {
-        self.stopStreamInMediaPlayback(function() {
-          self.stopMediaElement();
-          onSuccess();
-        }, onError);
-      }, onError);
+    value: function(isResume) {
+      return this.startMedia(isResume)
+        .then(() => this.stopStreamInMediaPlayback())
+        .then(() => this.stopMediaElement());
     }
   },
 
   /**
    * Stops the local media stream while it's currently in playback in
    * a media element.
    *
    * Precondition: The media stream and element should both be actively
    *               being played.
    *
-   * @param {Function} onSuccess the success callback if the media element
-   *                             fires an ended event from stop() being called
-   * @param {Function} onError the error callback if the media element
-   *                           fails to fire an ended event from stop() being
-   *                           called
    */
   stopStreamInMediaPlayback : {
-    value: function (onSuccess, onError) {
-      var endedFired = false;
-      var self = this;
+    value: function () {
+      return new Promise((resolve, reject) => {
+        /**
+         * Callback fired when the ended event fires when stop() is called on the
+         * stream.
+         */
+        var endedCallback = () => {
+          this.mediaElement.removeEventListener('ended', endedCallback, false);
+          ok(true, "ended event successfully fired");
+          resolve();
+        };
 
-      /**
-       * Callback fired when the ended event fires when stop() is called on the
-       * stream.
-       */
-      var endedCallback = function() {
-        endedFired = true;
-        self.mediaElement.removeEventListener('ended', endedCallback, false);
-        ok(true, "ended event successfully fired");
-        onSuccess();
-      };
+        this.mediaElement.addEventListener('ended', endedCallback, false);
+        this.mediaStream.stop();
 
-      this.mediaElement.addEventListener('ended', endedCallback, false);
-      this.mediaStream.stop();
-
-      // If ended doesn't fire in enough time, then we fail the test
-      setTimeout(function() {
-        if (!endedFired) {
-          onError("ended event never fired");
-        }
-      }, ENDED_TIMEOUT_LENGTH);
+        // If ended doesn't fire in enough time, then we fail the test
+        setTimeout(() => {
+          reject(new Error("ended event never fired"));
+        }, ENDED_TIMEOUT_LENGTH);
+      });
     }
   }
 });
+
+// haxx to prevent SimpleTest from failing at window.onload
+function addLoadEvent() {}
+
+var scriptsReady = Promise.all([
+  "/tests/SimpleTest/SimpleTest.js",
+  "head.js"
+].map(script  => {
+  var el = document.createElement("script");
+  el.src = script;
+  document.head.appendChild(el);
+  return new Promise(r => el.onload = r);
+}));
+
+function createHTML(options) {
+  return scriptsReady.then(() => realCreateHTML(options));
+}
+
+function runTest(f) {
+  return scriptsReady.then(() => runTestWhenReady(f));
+}
--- a/dom/media/tests/mochitest/mochitest.ini
+++ b/dom/media/tests/mochitest/mochitest.ini
@@ -1,16 +1,17 @@
 [DEFAULT]
 # strictContentSandbox - bug 1042735, Android 2.3 - bug 981881
 skip-if = (os == 'win' && strictContentSandbox) || android_version == '10'
 support-files =
   head.js
   constraints.js
   dataChannel.js
   mediaStreamPlayback.js
+  network.js
   nonTrickleIce.js
   pc.js
   templates.js
   NetworkPreparationChromeScript.js
   blacksilence.js
   turnConfig.js
 
 [test_dataChannel_basicAudio.html]
@@ -38,16 +39,17 @@ skip-if = buildapp == 'b2g' || toolkit =
 [test_getUserMedia_basicWindowshare.html]
 skip-if = buildapp == 'b2g' || toolkit == 'android' # no windowshare on b2g/android
 [test_getUserMedia_basicVideoAudio.html]
 skip-if = (toolkit == 'gonk' && debug) # debug-only failure, turned an intermittent (bug 962579) into a permanant orange
 [test_getUserMedia_constraints.html]
 skip-if = toolkit == 'gonk' || toolkit == 'android' # Bug 907352, backwards-compatible behavior on mobile only
 [test_getUserMedia_constraints_mobile.html]
 skip-if = toolkit != 'gonk' && toolkit != 'android' # Bug 907352, backwards-compatible behavior on mobile only
+[test_getUserMedia_callbacks.html]
 [test_getUserMedia_gumWithinGum.html]
 [test_getUserMedia_playAudioTwice.html]
 [test_getUserMedia_playVideoAudioTwice.html]
 skip-if = (toolkit == 'gonk' && debug) # debug-only failure; bug 926558
 [test_getUserMedia_playVideoTwice.html]
 [test_getUserMedia_stopAudioStream.html]
 [test_getUserMedia_stopAudioStreamWithFollowupAudio.html]
 [test_getUserMedia_stopVideoAudioStream.html]
@@ -106,16 +108,18 @@ skip-if = toolkit == 'gonk' # b2g (Bug 1
 [test_peerConnection_offerRequiresReceiveAudio.html]
 skip-if = toolkit == 'gonk' # b2g(Bug 960442, video support for WebRTC is disabled on b2g)
 [test_peerConnection_offerRequiresReceiveVideo.html]
 skip-if = toolkit == 'gonk' # b2g(Bug 960442, video support for WebRTC is disabled on b2g)
 [test_peerConnection_offerRequiresReceiveVideoAudio.html]
 skip-if = toolkit == 'gonk' # b2g(Bug 960442, video support for WebRTC is disabled on b2g)
 [test_peerConnection_promiseSendOnly.html]
 skip-if = toolkit == 'gonk' # b2g(Bug 960442, video support for WebRTC is disabled on b2g)
+[test_peerConnection_callbacks.html]
+skip-if = toolkit == 'gonk' # b2g(Bug 960442, video support for WebRTC is disabled on b2g)
 [test_peerConnection_replaceTrack.html]
 skip-if = toolkit == 'gonk' # b2g(Bug 960442, video support for WebRTC is disabled on b2g)
 [test_peerConnection_syncSetDescription.html]
 skip-if = toolkit == 'gonk' # b2g(Bug 960442, video support for WebRTC is disabled on b2g)
 [test_peerConnection_setLocalAnswerInHaveLocalOffer.html]
 skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
 [test_peerConnection_setLocalAnswerInStable.html]
 skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/network.js
@@ -0,0 +1,121 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * Query function for determining if any IP address is available for
+ * generating SDP.
+ *
+ * @return false if required additional network setup.
+ */
+function isNetworkReady() {
+  // for gonk platform
+  if ("nsINetworkInterfaceListService" in SpecialPowers.Ci) {
+    var listService = SpecialPowers.Cc["@mozilla.org/network/interface-list-service;1"]
+                        .getService(SpecialPowers.Ci.nsINetworkInterfaceListService);
+    var itfList = listService.getDataInterfaceList(
+          SpecialPowers.Ci.nsINetworkInterfaceListService.LIST_NOT_INCLUDE_MMS_INTERFACES |
+          SpecialPowers.Ci.nsINetworkInterfaceListService.LIST_NOT_INCLUDE_SUPL_INTERFACES |
+          SpecialPowers.Ci.nsINetworkInterfaceListService.LIST_NOT_INCLUDE_IMS_INTERFACES |
+          SpecialPowers.Ci.nsINetworkInterfaceListService.LIST_NOT_INCLUDE_DUN_INTERFACES);
+    var num = itfList.getNumberOfInterface();
+    for (var i = 0; i < num; i++) {
+      var ips = {};
+      var prefixLengths = {};
+      var length = itfList.getInterface(i).getAddresses(ips, prefixLengths);
+
+      for (var j = 0; j < length; j++) {
+        var ip = ips.value[j];
+        // skip IPv6 address until bug 797262 is implemented
+        if (ip.indexOf(":") < 0) {
+          info("Network interface is ready with address: " + ip);
+          return true;
+        }
+      }
+    }
+    // ip address is not available
+    info("Network interface is not ready, required additional network setup");
+    return false;
+  }
+  info("Network setup is not required");
+  return true;
+}
+
+/**
+ * Network setup utils for Gonk
+ *
+ * @return {object} providing functions for setup/teardown data connection
+ */
+function getNetworkUtils() {
+  var url = SimpleTest.getTestFileURL("NetworkPreparationChromeScript.js");
+  var script = SpecialPowers.loadChromeScript(url);
+
+  var utils = {
+    /**
+     * Utility for setting up data connection.
+     *
+     * @param aCallback callback after data connection is ready.
+     */
+    prepareNetwork: function() {
+      return new Promise(resolve => {
+        script.addMessageListener('network-ready', () =>  {
+          info("Network interface is ready");
+          resolve();
+        });
+        info("Setting up network interface");
+        script.sendAsyncMessage("prepare-network", true);
+      });
+    },
+    /**
+     * Utility for tearing down data connection.
+     *
+     * @param aCallback callback after data connection is closed.
+     */
+    tearDownNetwork: function() {
+      if (!isNetworkReady()) {
+        info("No network to tear down");
+        return Promise.resolve();
+      }
+      return new Promise(resolve => {
+        script.addMessageListener('network-disabled', message => {
+          info("Network interface torn down");
+          script.destroy();
+          resolve();
+        });
+        info("Tearing down network interface");
+        script.sendAsyncMessage("network-cleanup", true);
+      });
+    }
+  };
+
+  return utils;
+}
+
+/**
+ * Setup network on Gonk if needed and execute test once network is up
+ *
+ */
+function startNetworkAndTest() {
+  if (isNetworkReady()) {
+    return Promise.resolve();
+  }
+  var utils = getNetworkUtils();
+  // Trigger network setup to obtain IP address before creating any PeerConnection.
+  return utils.prepareNetwork();
+}
+
+/**
+ * A wrapper around SimpleTest.finish() to handle B2G network teardown
+ */
+function networkTestFinished() {
+  var p;
+  if ("nsINetworkInterfaceListService" in SpecialPowers.Ci) {
+    var utils = getNetworkUtils();
+    p = utils.tearDownNetwork();
+  } else {
+    p = Promise.resolve();
+  }
+  return p.then(() => finish());
+}
--- a/dom/media/tests/mochitest/nonTrickleIce.js
+++ b/dom/media/tests/mochitest/nonTrickleIce.js
@@ -1,130 +1,60 @@
 /* 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/. */
 
 function makeOffererNonTrickle(chain) {
   chain.replace('PC_LOCAL_SETUP_ICE_HANDLER', [
-    ['PC_LOCAL_SETUP_NOTRICKLE_ICE_HANDLER',
-      function (test) {
-        test.pcLocalWaitingForEndOfTrickleIce = false;
-        // We need to install this callback before calling setLocalDescription
-        // otherwise we might miss callbacks
-        test.pcLocal.setupIceCandidateHandler(test, function () {
-            // We ignore ICE candidates because we want the full offer
-          } , function (label) {
-            if (test.pcLocalWaitingForEndOfTrickleIce) {
-              // This callback is needed for slow environments where ICE
-              // trickling has not finished before the other side needs the
-              // full SDP. In this case, this call to test.next() will complete
-              // the PC_REMOTE_WAIT_FOR_OFFER step (see below).
-              info("Looks like we were still waiting for Trickle to finish");
-              // TODO replace this with a Promise
-              test.next();
-            }
-          });
-        // We can't wait for trickle to finish here as it will only start once
-        // we have called setLocalDescription in the next step
-        test.next();
-      }
-    ]
+    function PC_LOCAL_SETUP_NOTRICKLE_ICE_HANDLER(test) {
+      // We need to install this callback before calling setLocalDescription
+      // otherwise we might miss callbacks
+      test.pcLocal.setupIceCandidateHandler(test, () => {});
+      // We ignore ICE candidates because we want the full offer
+    }
   ]);
   chain.replace('PC_REMOTE_GET_OFFER', [
-    ['PC_REMOTE_WAIT_FOR_OFFER',
-      function (test) {
-        if (test.pcLocal.endOfTrickleIce) {
-          info("Trickle ICE finished already");
-          test.next();
-        } else {
-          info("Waiting for trickle ICE to finish");
-          test.pcLocalWaitingForEndOfTrickleIce = true;
-          // In this case we rely on the callback from
-          // PC_LOCAL_SETUP_NOTRICKLE_ICE_HANDLER above to proceed to the next
-          // step once trickle is finished.
-        }
-      }
-    ],
-    ['PC_REMOTE_GET_FULL_OFFER',
-      function (test) {
+    function PC_REMOTE_GET_FULL_OFFER(test) {
+      return test.pcLocal.endOfTrickleIce.then(() => {
         test._local_offer = test.pcLocal.localDescription;
         test._offer_constraints = test.pcLocal.constraints;
         test._offer_options = test.pcLocal.offerOptions;
-        test.next();
-      }
-    ]
+      });
+    }
   ]);
   chain.insertAfter('PC_REMOTE_SANE_REMOTE_SDP', [
-    ['PC_REMOTE_REQUIRE_REMOTE_SDP_CANDIDATES',
-      function (test) {
-        info("test.pcLocal.localDescription.sdp: " + JSON.stringify(test.pcLocal.localDescription.sdp));
-        info("test._local_offer.sdp" + JSON.stringify(test._local_offer.sdp));
-        ok(!test.localRequiresTrickleIce, "Local does NOT require trickle");
-        ok(test._local_offer.sdp.contains("a=candidate"), "offer has ICE candidates")
-        // TODO check for a=end-of-candidates once implemented
-        test.next();
-      }
-    ]
+    function PC_REMOTE_REQUIRE_REMOTE_SDP_CANDIDATES(test) {
+      info("test.pcLocal.localDescription.sdp: " + JSON.stringify(test.pcLocal.localDescription.sdp));
+      info("test._local_offer.sdp" + JSON.stringify(test._local_offer.sdp));
+      ok(!test.localRequiresTrickleIce, "Local does NOT require trickle");
+      ok(test._local_offer.sdp.contains("a=candidate"), "offer has ICE candidates")
+      ok(test._local_offer.sdp.contains("a=end-of-candidates"), "offer has end-of-candidates");
+    }
   ]);
 }
 
 function makeAnswererNonTrickle(chain) {
   chain.replace('PC_REMOTE_SETUP_ICE_HANDLER', [
-    ['PC_REMOTE_SETUP_NOTRICKLE_ICE_HANDLER',
-      function (test) {
-        test.pcRemoteWaitingForEndOfTrickleIce = false;
-        // We need to install this callback before calling setLocalDescription
-        // otherwise we might miss callbacks
-        test.pcRemote.setupIceCandidateHandler(test, function () {
-          // We ignore ICE candidates because we want the full answer
-          }, function (label) {
-            if (test.pcRemoteWaitingForEndOfTrickleIce) {
-              // This callback is needed for slow environments where ICE
-              // trickling has not finished before the other side needs the
-              // full SDP. In this case this callback will call the step after
-              // PC_LOCAL_WAIT_FOR_ANSWER
-              info("Looks like we were still waiting for Trickle to finish");
-              // TODO replace this with a Promise
-              test.next();
-            }
-          });
-        // We can't wait for trickle to finish here as it will only start once
-        // we have called setLocalDescription in the next step
-        test.next();
-      }
-    ]
+    function PC_REMOTE_SETUP_NOTRICKLE_ICE_HANDLER(test) {
+      // We need to install this callback before calling setLocalDescription
+      // otherwise we might miss callbacks
+      test.pcRemote.setupIceCandidateHandler(test, () => {});
+      // We ignore ICE candidates because we want the full offer
+    }
   ]);
   chain.replace('PC_LOCAL_GET_ANSWER', [
-    ['PC_LOCAL_WAIT_FOR_ANSWER',
-      function (test) {
-        if (test.pcRemote.endOfTrickleIce) {
-          info("Trickle ICE finished already");
-          test.next();
-        } else {
-          info("Waiting for trickle ICE to finish");
-          test.pcRemoteWaitingForEndOfTrickleIce = true;
-          // In this case we rely on the callback from
-          // PC_REMOTE_SETUP_NOTRICKLE_ICE_HANDLER above to proceed to the next
-          // step once trickle is finished.
-        }
-      }
-    ],
-    ['PC_LOCAL_GET_FULL_ANSWER',
-      function (test) {
+    function PC_LOCAL_GET_FULL_ANSWER(test) {
+      return test.pcRemote.endOfTrickleIce.then(() => {
         test._remote_answer = test.pcRemote.localDescription;
         test._answer_constraints = test.pcRemote.constraints;
-        test.next();
-      }
-    ]
+      });
+    }
   ]);
   chain.insertAfter('PC_LOCAL_SANE_REMOTE_SDP', [
-    ['PC_LOCAL_REQUIRE_REMOTE_SDP_CANDIDATES',
-      function (test) {
-        info("test.pcRemote.localDescription.sdp: " + JSON.stringify(test.pcRemote.localDescription.sdp));
-        info("test._remote_answer.sdp" + JSON.stringify(test._remote_answer.sdp));
-        ok(!test.remoteRequiresTrickleIce, "Remote does NOT require trickle");
-        ok(test._remote_answer.sdp.contains("a=candidate"), "answer has ICE candidates")
-        // TODO check for a=end-of-candidates once implemented
-        test.next();
-      }
-    ]
+    function PC_LOCAL_REQUIRE_REMOTE_SDP_CANDIDATES(test) {
+      info("test.pcRemote.localDescription.sdp: " + JSON.stringify(test.pcRemote.localDescription.sdp));
+      info("test._remote_answer.sdp" + JSON.stringify(test._remote_answer.sdp));
+      ok(!test.remoteRequiresTrickleIce, "Remote does NOT require trickle");
+      ok(test._remote_answer.sdp.contains("a=candidate"), "answer has ICE candidates")
+      ok(test._remote_answer.sdp.contains("a=end-of-candidates"), "answer has end-of-candidates");
+    }
   ]);
 }
--- a/dom/media/tests/mochitest/pc.js
+++ b/dom/media/tests/mochitest/pc.js
@@ -24,355 +24,91 @@ const signalingStateTransitions = {
   "have-local-offer": ["have-remote-pranswer", "stable", "closed", "have-local-offer"],
   "have-remote-pranswer": ["stable", "closed", "have-remote-pranswer"],
   "have-remote-offer": ["have-local-pranswer", "stable", "closed", "have-remote-offer"],
   "have-local-pranswer": ["stable", "closed", "have-local-pranswer"],
   "closed": []
 }
 
 /**
- * This class mimics a state machine and handles a list of commands by
- * executing them synchronously.
- *
- * @constructor
- * @param {object} framework
- *        A back reference to the framework which makes use of the class. It's
- *        getting passed in as parameter to each command callback.
- * @param {Array[]} [commandList=[]]
- *        Default commands to set during initialization
- */
-function CommandChain(framework, commandList) {
-  this._framework = framework;
-
-  this._commands = commandList || [ ];
-  this._current = 0;
-
-  this.onFinished = null;
-}
-
-CommandChain.prototype = {
-
-  /**
-   * Returns the index of the current command of the chain
-   *
-   * @returns {number} Index of the current command
-   */
-  get current() {
-    return this._current;
-  },
-
-  /**
-   * Checks if the chain has already processed all the commands
-   *
-   * @returns {boolean} True, if all commands have been processed
-   */
-  get finished() {
-    return this._current === this._commands.length;
-  },
-
-  /**
-   * Returns the assigned commands of the chain.
-   *
-   * @returns {Array[]} Commands of the chain
-   */
-  get commands() {
-    return this._commands;
-  },
-
-  /**
-   * Sets new commands for the chain. All existing commands will be replaced.
-   *
-   * @param {Array[]} commands
-   *        List of commands
-   */
-  set commands(commands) {
-    this._commands = commands;
-  },
-
-  /**
-   * Execute the next command in the chain.
-   */
-  executeNext : function () {
-    var self = this;
-
-    function _executeNext() {
-      if (!self.finished) {
-        var step = self._commands[self._current];
-        self._current++;
-
-        self.currentStepLabel = step[0];
-        info("Run step: " + self.currentStepLabel);
-        step[1](self._framework);      // Execute step
-      }
-      else if (typeof(self.onFinished) === 'function') {
-        self.onFinished();
-      }
-    }
-
-    // To prevent building up the stack we have to execute the next
-    // step asynchronously
-    window.setTimeout(_executeNext, 0);
-  },
-
-  /**
-   * Add new commands to the end of the chain
-   *
-   * @param {Array[]} commands
-   *        List of commands
-   */
-  append: function (commands) {
-    this._commands = this._commands.concat(commands);
-  },
-
-  /**
-   * Returns the index of the specified command in the chain.
-   *
-   * @param {string} id
-   *        Identifier of the command
-   * @returns {number} Index of the command
-   */
-  indexOf: function (id) {
-    for (var i = 0; i < this._commands.length; i++) {
-      if (this._commands[i][0] === id) {
-        return i;
-      }
-    }
-
-    return -1;
-  },
-
-  /**
-   * Inserts the new commands after the specified command.
-   *
-   * @param {string} id
-   *        Identifier of the command
-   * @param {Array[]} commands
-   *        List of commands
-   */
-  insertAfter: function (id, commands) {
-    var index = this.indexOf(id);
-
-    if (index > -1) {
-      var tail = this.removeAfter(id);
-
-      this.append(commands);
-      this.append(tail);
-    }
-  },
-
-  /**
-   * Inserts the new commands before the specified command.
-   *
-   * @param {string} id
-   *        Identifier of the command
-   * @param {Array[]} commands
-   *        List of commands
-   */
-  insertBefore: function (id, commands) {
-    var index = this.indexOf(id);
-
-    if (index > -1) {
-      var tail = this.removeAfter(id);
-      var object = this.remove(id);
-
-      this.append(commands);
-      this.append(object);
-      this.append(tail);
-    }
-  },
-
-  /**
-   * Removes the specified command
-   *
-   * @param {string} id
-   *        Identifier of the command
-   * @returns {object[]} Removed command
-   */
-  remove : function (id) {
-    return this._commands.splice(this.indexOf(id), 1);
-  },
-
-  /**
-   * Removes all commands after the specified one.
-   *
-   * @param {string} id
-   *        Identifier of the command
-   * @returns {object[]} Removed commands
-   */
-  removeAfter : function (id) {
-    var index = this.indexOf(id);
-
-    if (index > -1) {
-      return this._commands.splice(index + 1);
-    }
-
-    return null;
-  },
-
-  /**
-   * Removes all commands before the specified one.
-   *
-   * @param {string} id
-   *        Identifier of the command
-   * @returns {object[]} Removed commands
-   */
-  removeBefore : function (id) {
-    var index = this.indexOf(id);
-
-    if (index > -1) {
-      return this._commands.splice(0, index);
-    }
-
-    return null;
-  },
-
-  /**
-   * Replaces a single command.
-   *
-   * @param {string} id
-   *        Identifier of the command to be replaced
-   * @param {Array[]} commands
-   *        List of commands
-   * @returns {object[]} Removed commands
-   */
-  replace : function (id, commands) {
-    this.insertBefore(id, commands);
-    return this.remove(id);
-  },
-
-  /**
-   * Replaces all commands after the specified one.
-   *
-   * @param {string} id
-   *        Identifier of the command
-   * @returns {object[]} Removed commands
-   */
-  replaceAfter : function (id, commands) {
-    var oldCommands = this.removeAfter(id);
-    this.append(commands);
-
-    return oldCommands;
-  },
-
-  /**
-   * Replaces all commands before the specified one.
-   *
-   * @param {string} id
-   *        Identifier of the command
-   * @returns {object[]} Removed commands
-   */
-  replaceBefore : function (id, commands) {
-    var oldCommands = this.removeBefore(id);
-    this.insertBefore(id, commands);
-
-    return oldCommands;
-  },
-
-  /**
-   * Remove all commands whose identifiers match the specified regex.
-   *
-   * @param {regex} id_match
-   *        Regular expression to match command identifiers.
-   */
-  filterOut : function (id_match) {
-    for (var i = this._commands.length - 1; i >= 0; i--) {
-      if (id_match.test(this._commands[i][0])) {
-        this._commands.splice(i, 1);
-      }
-    }
-  }
-};
-
-/**
  * This class provides a state checker for media elements which store
  * a media stream to check for media attribute state and events fired.
  * When constructed by a caller, an object instance is created with
  * a media element, event state checkers for canplaythrough, timeupdate, and
  * time changing on the media element and stream.
  *
  * @param {HTMLMediaElement} element the media element being analyzed
  */
 function MediaElementChecker(element) {
   this.element = element;
   this.canPlayThroughFired = false;
   this.timeUpdateFired = false;
   this.timePassed = false;
 
-  var self = this;
-  var elementId = self.element.getAttribute('id');
+  var elementId = this.element.getAttribute('id');
 
   // When canplaythrough fires, we track that it's fired and remove the
   // event listener.
-  var canPlayThroughCallback = function() {
+  var canPlayThroughCallback = () => {
     info('canplaythrough fired for media element ' + elementId);
-    self.canPlayThroughFired = true;
-    self.element.removeEventListener('canplaythrough', canPlayThroughCallback,
+    this.canPlayThroughFired = true;
+    this.element.removeEventListener('canplaythrough', canPlayThroughCallback,
                                      false);
   };
 
   // When timeupdate fires, we track that it's fired and check if time
   // has passed on the media stream and media element.
-  var timeUpdateCallback = function() {
-    self.timeUpdateFired = true;
+  var timeUpdateCallback = () => {
+    this.timeUpdateFired = true;
     info('timeupdate fired for media element ' + elementId);
 
     // If time has passed, then track that and remove the timeupdate event
     // listener.
     if(element.mozSrcObject && element.mozSrcObject.currentTime > 0 &&
        element.currentTime > 0) {
       info('time passed for media element ' + elementId);
-      self.timePassed = true;
-      self.element.removeEventListener('timeupdate', timeUpdateCallback,
+      this.timePassed = true;
+      this.element.removeEventListener('timeupdate', timeUpdateCallback,
                                        false);
     }
   };
 
   element.addEventListener('canplaythrough', canPlayThroughCallback, false);
   element.addEventListener('timeupdate', timeUpdateCallback, false);
 }
 
 MediaElementChecker.prototype = {
 
   /**
    * Waits until the canplaythrough & timeupdate events to fire along with
    * ensuring time has passed on the stream and media element.
-   *
-   * @param {Function} onSuccess the success callback when media flow is
-   *                             established
    */
-  waitForMediaFlow : function MEC_WaitForMediaFlow(onSuccess) {
-    var self = this;
-    var elementId = self.element.getAttribute('id');
+  waitForMediaFlow: function() {
+    var elementId = this.element.getAttribute('id');
     info('Analyzing element: ' + elementId);
 
-    if(self.canPlayThroughFired && self.timeUpdateFired && self.timePassed) {
-      ok(true, 'Media flowing for ' + elementId);
-      onSuccess();
-    } else {
-      setTimeout(function() {
-        self.waitForMediaFlow(onSuccess);
-      }, 100);
-    }
+    return waitUntil(() => this.canPlayThroughFired && this.timeUpdateFired && this.timePassed)
+      .then(() => ok(true, 'Media flowing for ' + elementId));
   },
 
   /**
    * Checks if there is no media flow present by checking that the ready
    * state of the media element is HAVE_METADATA.
    */
-  checkForNoMediaFlow : function MEC_CheckForNoMediaFlow() {
+  checkForNoMediaFlow: function() {
     ok(this.element.readyState === HTMLMediaElement.HAVE_METADATA,
        'Media element has a ready state of HAVE_METADATA');
   }
 };
 
 /**
  * Only calls info() if SimpleTest.info() is available
  */
 function safeInfo(message) {
-  if (typeof(info) === "function") {
+  if (typeof info === "function") {
     info(message);
   }
 }
 
 // Also remove mode 0 if it's offered
 // Note, we don't bother removing the fmtp lines, which makes a good test
 // for some SDP parsing issues.
 function removeVP8(sdp) {
@@ -381,138 +117,16 @@ function removeVP8(sdp) {
   updated_sdp = updated_sdp.replace("RTP/SAVPF 120 126\r\n","RTP/SAVPF 126\r\n");
   updated_sdp = updated_sdp.replace("a=rtcp-fb:120 nack\r\n","");
   updated_sdp = updated_sdp.replace("a=rtcp-fb:120 nack pli\r\n","");
   updated_sdp = updated_sdp.replace("a=rtcp-fb:120 ccm fir\r\n","");
   return updated_sdp;
 }
 
 /**
- * Query function for determining if any IP address is available for
- * generating SDP.
- *
- * @return false if required additional network setup.
- */
-function isNetworkReady() {
-  // for gonk platform
-  if ("nsINetworkInterfaceListService" in SpecialPowers.Ci) {
-    var listService = SpecialPowers.Cc["@mozilla.org/network/interface-list-service;1"]
-                        .getService(SpecialPowers.Ci.nsINetworkInterfaceListService);
-    var itfList = listService.getDataInterfaceList(
-          SpecialPowers.Ci.nsINetworkInterfaceListService.LIST_NOT_INCLUDE_MMS_INTERFACES |
-          SpecialPowers.Ci.nsINetworkInterfaceListService.LIST_NOT_INCLUDE_SUPL_INTERFACES |
-          SpecialPowers.Ci.nsINetworkInterfaceListService.LIST_NOT_INCLUDE_IMS_INTERFACES |
-          SpecialPowers.Ci.nsINetworkInterfaceListService.LIST_NOT_INCLUDE_DUN_INTERFACES);
-    var num = itfList.getNumberOfInterface();
-    for (var i = 0; i < num; i++) {
-      var ips = {};
-      var prefixLengths = {};
-      var length = itfList.getInterface(i).getAddresses(ips, prefixLengths);
-
-      for (var j = 0; j < length; j++) {
-        var ip = ips.value[j];
-        // skip IPv6 address until bug 797262 is implemented
-        if (ip.indexOf(":") < 0) {
-          safeInfo("Network interface is ready with address: " + ip);
-          return true;
-        }
-      }
-    }
-    // ip address is not available
-    safeInfo("Network interface is not ready, required additional network setup");
-    return false;
-  }
-  safeInfo("Network setup is not required");
-  return true;
-}
-
-/**
- * Network setup utils for Gonk
- *
- * @return {object} providing functions for setup/teardown data connection
- */
-function getNetworkUtils() {
-  var url = SimpleTest.getTestFileURL("NetworkPreparationChromeScript.js");
-  var script = SpecialPowers.loadChromeScript(url);
-
-  var utils = {
-    /**
-     * Utility for setting up data connection.
-     *
-     * @param aCallback callback after data connection is ready.
-     */
-    prepareNetwork: function(onSuccess) {
-      script.addMessageListener('network-ready', function (message) {
-        info("Network interface is ready");
-        onSuccess();
-      });
-      info("Setting up network interface");
-      script.sendAsyncMessage("prepare-network", true);
-    },
-    /**
-     * Utility for tearing down data connection.
-     *
-     * @param aCallback callback after data connection is closed.
-     */
-    tearDownNetwork: function(onSuccess, onFailure) {
-      if (isNetworkReady()) {
-        script.addMessageListener('network-disabled', function (message) {
-          info("Network interface torn down");
-          script.destroy();
-          onSuccess();
-        });
-        info("Tearing down network interface");
-        script.sendAsyncMessage("network-cleanup", true);
-      } else {
-        info("No network to tear down");
-        onFailure();
-      }
-    }
-  };
-
-  return utils;
-}
-
-/**
- * Setup network on Gonk if needed and execute test once network is up
- *
- */
-function startNetworkAndTest(onSuccess) {
-  if (!isNetworkReady()) {
-    SimpleTest.waitForExplicitFinish();
-    var utils = getNetworkUtils();
-    // Trigger network setup to obtain IP address before creating any PeerConnection.
-    utils.prepareNetwork(onSuccess);
-  } else {
-    onSuccess();
-  }
-}
-
-/**
- * A wrapper around SimpleTest.finish() to handle B2G network teardown
- */
-function networkTestFinished() {
-  if ("nsINetworkInterfaceListService" in SpecialPowers.Ci) {
-    var utils = getNetworkUtils();
-    utils.tearDownNetwork(SimpleTest.finish, SimpleTest.finish);
-  } else {
-    SimpleTest.finish();
-  }
-}
-
-/**
- * A wrapper around runTest() which handles B2G network setup and teardown
- */
-function runNetworkTest(testFunction) {
-  startNetworkAndTest(function() {
-    runTest(testFunction);
-  });
-}
-
-/**
  * This class handles tests for peer connections.
  *
  * @constructor
  * @param {object} [options={}]
  *        Optional options for the peer connection test
  * @param {object} [options.commands=commandsPeerConnection]
  *        Commands to run for the test
  * @param {bool}   [options.is_local=true]
@@ -566,637 +180,337 @@ function PeerConnectionTest(options) {
   // Create command chain instance and assign default commands
   this.chain = new CommandChain(this, options.commands);
   if (!options.is_local) {
     this.chain.filterOut(/^PC_LOCAL/);
   }
   if (!options.is_remote) {
     this.chain.filterOut(/^PC_REMOTE/);
   }
+}
 
-  var self = this;
-  this.chain.onFinished = function () {
-    self.teardown();
-  };
+/** TODO: consider removing this dependency on timeouts */
+function timerGuard(p, time, message) {
+  return Promise.race([
+    p,
+    wait(time).then(() => {
+      throw new Error('timeout after ' + (time / 1000) + 's: ' + message);
+    })
+  ]);
 }
 
 /**
  * Closes the peer connection if it is active
  *
  * @param {Function} onSuccess
  *        Callback to execute when the peer connection has been closed successfully
  */
-PeerConnectionTest.prototype.closePC = function PCT_closePC(onSuccess) {
+PeerConnectionTest.prototype.closePC = function() {
   info("Closing peer connections");
 
-  var self = this;
-  var closeTimeout = null;
-  var waitingForLocal = false;
-  var waitingForRemote = false;
-  var everythingClosed = false;
-
-  function verifyClosed() {
-    if ((self.waitingForLocal || self.waitingForRemote) ||
-      (self.pcLocal && (self.pcLocal.signalingState !== "closed")) ||
-      (self.pcRemote && (self.pcRemote.signalingState !== "closed"))) {
-      info("still waiting for closure");
+  var closeIt = pc => {
+    if (!pc || pc.signalingState === "closed") {
+      return Promise.resolve();
     }
-    else if (!everythingClosed) {
-      info("No closure pending");
-      if (self.pcLocal) {
-        is(self.pcLocal.signalingState, "closed", "pcLocal is in 'closed' state");
-      }
-      if (self.pcRemote) {
-        is(self.pcRemote.signalingState, "closed", "pcRemote is in 'closed' state");
-      }
-      clearTimeout(closeTimeout);
-      everythingClosed = true;
-      onSuccess();
-    }
-  }
-
-  function signalingstatechangeLocalClose(e) {
-    info("'signalingstatechange' event received");
-    is(e.target.signalingState, "closed", "signalingState is closed");
-    self.waitingForLocal = false;
-    verifyClosed();
-  }
 
-  function signalingstatechangeRemoteClose(e) {
-    info("'signalingstatechange' event received");
-    is(e.target.signalingState, "closed", "signalingState is closed");
-    self.waitingForRemote = false;
-    verifyClosed();
-  }
+    return new Promise(resolve => {
+      pc.onsignalingstatechange = e => {
+        is(e.target.signalingState, "closed", "signalingState is closed");
+        resolve();
+      };
+      pc.close();
+    });
+  };
 
-  function closeEverything() {
-    if ((self.pcLocal) && (self.pcLocal.signalingState !== "closed")) {
-      info("Closing pcLocal");
-      self.pcLocal.onsignalingstatechange = signalingstatechangeLocalClose;
-      self.waitingForLocal = true;
-      self.pcLocal.close();
-    }
-    if ((self.pcRemote) && (self.pcRemote.signalingState !== "closed")) {
-      info("Closing pcRemote");
-      self.pcRemote.onsignalingstatechange = signalingstatechangeRemoteClose;
-      self.waitingForRemote = true;
-      self.pcRemote.close();
-    }
-    // give the signals handlers time to fire
-    setTimeout(verifyClosed, 1000);
-  }
-
-  closeTimeout = setTimeout(function() {
-    var closed = ((self.pcLocal && (self.pcLocal.signalingState === "closed")) &&
-      (self.pcRemote && (self.pcRemote.signalingState === "closed")));
-    ok(closed, "Closing PeerConnections timed out");
-    // it is not a success, but the show must go on
-    onSuccess();
-  }, 60000);
-
-  closeEverything();
+  return timerGuard(Promise.all([
+    closeIt(this.pcLocal),
+    closeIt(this.pcRemote)
+  ]), 60000, "failed to close peer connection");
 };
 
 /**
  * Close the open data channels, followed by the underlying peer connection
- *
- * @param {Function} onSuccess
- *        Callback to execute when all connections have been closed
  */
-PeerConnectionTest.prototype.close = function PCT_close(onSuccess) {
-  var self = this;
-  var pendingDcClose = []
-  var closeTimeout = null;
-
-  info("PeerConnectionTest.close() called");
-
-  function _closePeerConnection() {
-    info("Now closing PeerConnection");
-    self.closePC.call(self, onSuccess);
-  }
-
-  function _closePeerConnectionCallback(index) {
-    info("_closePeerConnection called with index " + index);
-    var pos = pendingDcClose.indexOf(index);
-    if (pos != -1) {
-      pendingDcClose.splice(pos, 1);
-    }
-    else {
-      info("_closePeerConnection index " + index + " is missing from pendingDcClose: " + pendingDcClose);
-    }
-    if (pendingDcClose.length === 0) {
-      clearTimeout(closeTimeout);
-      _closePeerConnection();
-    }
-  }
-
-  var myDataChannels = null;
-  if (self.pcLocal) {
-    myDataChannels = self.pcLocal.dataChannels;
-  }
-  else if (self.pcRemote) {
-    myDataChannels = self.pcRemote.dataChannels;
-  }
-  var length = myDataChannels.length;
-  for (var i = 0; i < length; i++) {
-    var dataChannel = myDataChannels[i];
-    if (dataChannel.readyState !== "closed") {
-      pendingDcClose.push(i);
-      self.closeDataChannels(i, _closePeerConnectionCallback);
-    }
-  }
-  if (pendingDcClose.length === 0) {
-    _closePeerConnection();
-  }
-  else {
-    closeTimeout = setTimeout(function() {
-      ok(false, "Failed to properly close data channels: " +
-        pendingDcClose);
-      _closePeerConnection();
-    }, 60000);
-  }
+PeerConnectionTest.prototype.close = function() {
+  var allChannels = (this.pcLocal || this.pcRemote).dataChannels;
+  return timerGuard(
+    Promise.all(allChannels.map((channel, i) => this.closeDataChannels(i))),
+    60000, "failed to close data channels")
+    .then(() => this.closePC());
 };
 
 /**
  * Close the specified data channels
  *
  * @param {Number} index
  *        Index of the data channels to close on both sides
- * @param {Function} onSuccess
- *        Callback to execute when the data channels has been closed
  */
-PeerConnectionTest.prototype.closeDataChannels = function PCT_closeDataChannels(index, onSuccess) {
+PeerConnectionTest.prototype.closeDataChannels = function(index) {
   info("closeDataChannels called with index: " + index);
   var localChannel = null;
   if (this.pcLocal) {
     localChannel = this.pcLocal.dataChannels[index];
   }
   var remoteChannel = null;
   if (this.pcRemote) {
     remoteChannel = this.pcRemote.dataChannels[index];
   }
 
-  var self = this;
-  var wait = false;
-  var pollingMode = false;
-  var everythingClosed = false;
-  var verifyInterval = null;
-  var remoteCloseTimer = null;
-
-  function _allChannelsAreClosed() {
-    var ret = null;
-    if (localChannel) {
-      ret = (localChannel.readyState === "closed");
-    }
-    if (remoteChannel) {
-      if (ret !== null) {
-        ret = (ret && (remoteChannel.readyState === "closed"));
-      }
-      else {
-        ret = (remoteChannel.readyState === "closed");
-      }
+  // We need to setup all the close listeners before calling close
+  var setupClosePromise = channel => {
+    if (!channel) {
+      return Promise.resolve();
     }
-    return ret;
-  }
+    return new Promise(resolve => {
+      channel.onclose = () => {
+        is(channel.readyState, "closed", name + " channel " + index + " closed");
+        resolve();
+      };
+    });
+  };
 
-  function verifyClosedChannels() {
-    if (everythingClosed) {
-      // safety protection against events firing late
-      return;
-    }
-    if (_allChannelsAreClosed()) {
-      ok(true, "DataChannel(s) have reached 'closed' state for data channel " + index);
-      if (remoteCloseTimer !== null) {
-        clearTimeout(remoteCloseTimer);
-      }
-      if (verifyInterval !== null) {
-        clearInterval(verifyInterval);
-      }
-      everythingClosed = true;
-      onSuccess(index);
-    }
-    else {
-      info("Still waiting for DataChannel closure");
-    }
+  // make sure to setup close listeners before triggering any actions
+  var allClosed = Promise.all([
+    setupClosePromise(localChannel),
+    setupClosePromise(remoteChannel)
+  ]);
+  var complete = timerGuard(allClosed, 60000, "failed to close data channel pair");
+
+  // triggering close on one side should suffice
+  if (remoteChannel) {
+    remoteChannel.close();
+  } else if (localChannel) {
+    localChannel.close();
   }
 
-  if ((localChannel) && (localChannel.readyState !== "closed")) {
-    // in case of steeplechase there is no far end, so we can only poll
-    if (remoteChannel) {
-      remoteChannel.onclose = function () {
-        is(remoteChannel.readyState, "closed", "remoteChannel is in state 'closed'");
-        verifyClosedChannels();
-      };
-    }
-    else {
-      pollingMode = true;
-      verifyInterval = setInterval(verifyClosedChannels, 1000);
-    }
-
-    localChannel.close();
-    wait = true;
-  }
-  if ((remoteChannel) && (remoteChannel.readyState !== "closed")) {
-    if (localChannel) {
-      localChannel.onclose = function () {
-        is(localChannel.readyState, "closed", "localChannel is in state 'closed'");
-        verifyClosedChannels();
-      };
-
-      // Apparently we are running a local test which has both ends of the
-      // data channel locally available, so by default lets wait for the
-      // remoteChannel.onclose handler from above to confirm closure on both
-      // ends.
-      remoteCloseTimer = setTimeout(function() {
-        todo(false, "localChannel.close() did not resulted in close signal on remote side");
-        remoteChannel.close();
-        verifyClosedChannels();
-      }, 30000);
-    }
-    else {
-      pollingMode = true;
-      verifyTimer = setInterval(verifyClosedChannels, 1000);
-
-      remoteChannel.close();
-    }
-
-    wait = true;
-  }
-
-  if (!wait) {
-    onSuccess(index);
-  }
-};
-
-
-/**
- * Wait for the initial data channel to get into the open state
- *
- * @param {PeerConnectionWrapper} peer
- *        The peer connection wrapper to run the command on
- * @param {Function} onSuccess
- *        Callback when the creation was successful
- */
-PeerConnectionTest.prototype.waitForInitialDataChannel =
-        function PCT_waitForInitialDataChannel(peer, onSuccess, onFailure) {
-  var dcConnectionTimeout = null;
-  var dcOpened = false;
-
-  function dataChannelConnected(channel) {
-    // in case the switch statement below had called onSuccess already we
-    // don't want to call it again
-    if (!dcOpened) {
-      clearTimeout(dcConnectionTimeout);
-      is(channel.readyState, "open", peer + " dataChannels[0] switched to state: 'open'");
-      dcOpened = true;
-      onSuccess();
-    } else {
-      info("dataChannelConnected() called, but data channel was open already");
-    }
-  }
-
-  // TODO: drno: convert dataChannels into an object and make
-  //             registerDataChannelOpenEvent a generic function
-  if (peer == this.pcLocal) {
-    peer.dataChannels[0].onopen = dataChannelConnected;
-  } else {
-    peer.registerDataChannelOpenEvents(dataChannelConnected);
-  }
-
-  if (peer.dataChannels.length >= 1) {
-    // snapshot of the live value as it might change during test execution
-    const readyState = peer.dataChannels[0].readyState;
-    switch (readyState) {
-      case "open": {
-        is(readyState, "open", peer + " dataChannels[0] is already in state: 'open'");
-        dcOpened = true;
-        onSuccess();
-        break;
-      }
-      case "connecting": {
-        is(readyState, "connecting", peer + " dataChannels[0] is in state: 'connecting'");
-        if (onFailure) {
-          dcConnectionTimeout = setTimeout(function () {
-            is(peer.dataChannels[0].readyState, "open", peer + " timed out while waiting for dataChannels[0] to open");
-            onFailure();
-          }, 60000);
-        }
-        break;
-      }
-      default: {
-        ok(false, "dataChannels[0] is in unexpected state " + readyState);
-        if (onFailure) {
-          onFailure()
-        }
-      }
-    }
-  }
+  return complete;
 };
 
 /**
  * Send data (message or blob) to the other peer
  *
  * @param {String|Blob} data
  *        Data to send to the other peer. For Blobs the MIME type will be lost.
- * @param {Function} onSuccess
- *        Callback to execute when data has been sent
  * @param {Object} [options={ }]
  *        Options to specify the data channels to be used
  * @param {DataChannelWrapper} [options.sourceChannel=pcLocal.dataChannels[length - 1]]
  *        Data channel to use for sending the message
  * @param {DataChannelWrapper} [options.targetChannel=pcRemote.dataChannels[length - 1]]
  *        Data channel to use for receiving the message
  */
-PeerConnectionTest.prototype.send = function PCT_send(data, onSuccess, options) {
+PeerConnectionTest.prototype.send = function(data, options) {
   options = options || { };
   var source = options.sourceChannel ||
            this.pcLocal.dataChannels[this.pcLocal.dataChannels.length - 1];
   var target = options.targetChannel ||
            this.pcRemote.dataChannels[this.pcRemote.dataChannels.length - 1];
 
-  // Register event handler for the target channel
-  target.onmessage = function (recv_data) {
-    onSuccess(target, recv_data);
-  };
+  return new Promise(resolve => {
+    // Register event handler for the target channel
+    target.onmessage = e => {
+      resolve({ channel: target, data: e.data });
+    };
 
-  source.send(data);
+    source.send(data);
+  });
 };
 
 /**
  * Create a data channel
  *
  * @param {Dict} options
  *        Options for the data channel (see nsIPeerConnection)
- * @param {Function} onSuccess
- *        Callback when the creation was successful
  */
-PeerConnectionTest.prototype.createDataChannel = function DCT_createDataChannel(options, onSuccess) {
-  var localChannel = null;
-  var remoteChannel = null;
-  var self = this;
-
-  // Method to synchronize all asynchronous events.
-  function check_next_test() {
-    if (localChannel && remoteChannel) {
-      onSuccess(localChannel, remoteChannel);
-    }
+PeerConnectionTest.prototype.createDataChannel = function(options) {
+  var remotePromise;
+  if (!options.negotiated) {
+    this.pcRemote.expectDataChannel();
+    remotePromise = this.pcRemote.nextDataChannel;
   }
 
-  if (!options.negotiated) {
-    // Register handlers for the remote peer
-    this.pcRemote.registerDataChannelOpenEvents(function (channel) {
-      remoteChannel = channel;
-      check_next_test();
+  // Create the datachannel
+  var localChannel = this.pcLocal.createDataChannel(options)
+  var localPromise = localChannel.opened;
+
+  if (options.negotiated) {
+    remotePromise = localPromise.then(localChannel => {
+      // externally negotiated - we need to open from both ends
+      options.id = options.id || channel.id;  // allow for no id on options
+      var remoteChannel = this.pcRemote.createDataChannel(options);
+      return remoteChannel.opened;
     });
   }
 
-  // Create the datachannel and handle the local 'onopen' event
-  this.pcLocal.createDataChannel(options, function (channel) {
-    localChannel = channel;
-
-    if (options.negotiated) {
-      // externally negotiated - we need to open from both ends
-      options.id = options.id || channel.id;  // allow for no id to let the impl choose
-      self.pcRemote.createDataChannel(options, function (channel) {
-        remoteChannel = channel;
-        check_next_test();
-      });
-    } else {
-      check_next_test();
-    }
+  return Promise.all([localPromise, remotePromise]).then(result => {
+    return { local: result[0], remote: result[1] };
   });
 };
 
 /**
- * Executes the next command.
- */
-PeerConnectionTest.prototype.next = function PCT_next() {
-  if (this._stepTimeout) {
-    clearTimeout(this._stepTimeout);
-    this._stepTimeout = null;
-  }
-  this.chain.executeNext();
-};
-
-/**
- * Set a timeout for the current step.
- * @param {long] ms the number of milliseconds to allow for this step
- */
-PeerConnectionTest.prototype.setStepTimeout = function(ms) {
-  this._stepTimeout = setTimeout(function() {
-    ok(false, "Step timed out: " + this.chain.currentStepLabel);
-    this.next();
-  }.bind(this), ms);
-};
-
-/**
- * Set a timeout for the over all PeerConnectionTest
- * @param {long] ms the number of milliseconds to allow for the test
- */
-PeerConnectionTest.prototype.setTimeout = function(ms) {
-  this._timeout = setTimeout(function() {
-    ok(false, "PeerConnectionTest timed out");
-    this.teardown();
-  }.bind(this), ms);
-};
-
-/**
  * Creates an answer for the specified peer connection instance
  * and automatically handles the failure case.
  *
  * @param {PeerConnectionWrapper} peer
  *        The peer connection wrapper to run the command on
- * @param {function} onSuccess
- *        Callback to execute if the offer was created successfully
  */
-PeerConnectionTest.prototype.createAnswer =
-function PCT_createAnswer(peer, onSuccess) {
-  var self = this;
-
-  peer.createAnswer(function (answer) {
+PeerConnectionTest.prototype.createAnswer = function(peer) {
+  return peer.createAnswer().then(answer => {
     // make a copy so this does not get updated with ICE candidates
-    self.originalAnswer = new mozRTCSessionDescription(JSON.parse(JSON.stringify(answer)));
-    onSuccess(answer);
+    this.originalAnswer = new mozRTCSessionDescription(JSON.parse(JSON.stringify(answer)));
+    return answer;
   });
 };
 
 /**
  * Creates an offer for the specified peer connection instance
  * and automatically handles the failure case.
  *
  * @param {PeerConnectionWrapper} peer
  *        The peer connection wrapper to run the command on
- * @param {function} onSuccess
- *        Callback to execute if the offer was created successfully
  */
-PeerConnectionTest.prototype.createOffer =
-function PCT_createOffer(peer, onSuccess) {
-  var self = this;
-
-  peer.createOffer(function (offer) {
+PeerConnectionTest.prototype.createOffer = function(peer) {
+  return peer.createOffer().then(offer => {
     // make a copy so this does not get updated with ICE candidates
-    self.originalOffer = new mozRTCSessionDescription(JSON.parse(JSON.stringify(offer)));
-    onSuccess(offer);
+    this.originalOffer = new mozRTCSessionDescription(JSON.parse(JSON.stringify(offer)));
+    return offer;
   });
 };
 
 PeerConnectionTest.prototype.setIdentityProvider =
 function(peer, provider, protocol, identity) {
   peer.setIdentityProvider(provider, protocol, identity);
 };
 
 /**
  * Sets the local description for the specified peer connection instance
  * and automatically handles the failure case.
  *
  * @param {PeerConnectionWrapper} peer
           The peer connection wrapper to run the command on
  * @param {mozRTCSessionDescription} desc
  *        Session description for the local description request
- * @param {function} onSuccess
- *        Callback to execute if the local description was set successfully
  */
 PeerConnectionTest.prototype.setLocalDescription =
-function PCT_setLocalDescription(peer, desc, stateExpected, onSuccess) {
-  var eventFired = false;
-  var stateChanged = false;
-
-  function check_next_test() {
-    if (eventFired && stateChanged) {
-      onSuccess();
-    }
-  }
+function(peer, desc, stateExpected) {
+  var eventFired = new Promise(resolve => {
+    peer.onsignalingstatechange = e => {
+      info(peer + ": 'signalingstatechange' event received");
+      var state = e.target.signalingState;
+      if (stateExpected === state) {
+        peer.setLocalDescStableEventDate = new Date();
+        resolve();
+      } else {
+        ok(false, "This event has either already fired or there has been a " +
+           "mismatch between event received " + state +
+           " and event expected " + stateExpected);
+      }
+    };
+  });
 
-  peer.onsignalingstatechange = function (e) {
-    info(peer + ": 'signalingstatechange' event received");
-    var state = e.target.signalingState;
-    if(stateExpected === state && !eventFired) {
-      eventFired = true;
-      peer.setLocalDescStableEventDate = new Date();
-      check_next_test();
-    } else {
-      ok(false, "This event has either already fired or there has been a " +
-                "mismatch between event received " + state +
-                " and event expected " + stateExpected);
-    }
-  };
+  var stateChanged = peer.setLocalDescription(desc).then(() => {
+    peer.setLocalDescDate = new Date();
+  });
 
-  peer.setLocalDescription(desc, function () {
-    stateChanged = true;
-    peer.setLocalDescDate = new Date();
-    check_next_test();
-  });
+  return Promise.all([eventFired, stateChanged]);
 };
 
 /**
  * Sets the media constraints for both peer connection instances.
  *
  * @param {object} constraintsLocal
  *        Media constrains for the local peer connection instance
  * @param constraintsRemote
  */
 PeerConnectionTest.prototype.setMediaConstraints =
-function PCT_setMediaConstraints(constraintsLocal, constraintsRemote) {
-  if (this.pcLocal)
+function(constraintsLocal, constraintsRemote) {
+  if (this.pcLocal) {
     this.pcLocal.constraints = constraintsLocal;
-  if (this.pcRemote)
+  }
+  if (this.pcRemote) {
     this.pcRemote.constraints = constraintsRemote;
+  }
 };
 
 /**
  * Sets the media options used on a createOffer call in the test.
  *
  * @param {object} options the media constraints to use on createOffer
  */
-PeerConnectionTest.prototype.setOfferOptions =
-function PCT_setOfferOptions(options) {
-  if (this.pcLocal)
+PeerConnectionTest.prototype.setOfferOptions = function(options) {
+  if (this.pcLocal) {
     this.pcLocal.offerOptions = options;
+  }
 };
 
 /**
  * Sets the remote description for the specified peer connection instance
  * and automatically handles the failure case.
  *
  * @param {PeerConnectionWrapper} peer
           The peer connection wrapper to run the command on
  * @param {mozRTCSessionDescription} desc
  *        Session description for the remote description request
- * @param {function} onSuccess
- *        Callback to execute if the local description was set successfully
  */
 PeerConnectionTest.prototype.setRemoteDescription =
-function PCT_setRemoteDescription(peer, desc, stateExpected, onSuccess) {
-  var eventFired = false;
-  var stateChanged = false;
-
-  function check_next_test() {
-    if (eventFired && stateChanged) {
-      onSuccess();
-    }
-  }
+function(peer, desc, stateExpected) {
+  var eventFired = new Promise(resolve => {
+    peer.onsignalingstatechange = e => {
+      info(peer + ": 'signalingstatechange' event received");
+      var state = e.target.signalingState;
+      if (stateExpected === state) {
+        peer.setRemoteDescStableEventDate = new Date();
+        resolve();
+      } else {
+        ok(false, "This event has either already fired or there has been a " +
+           "mismatch between event received " + state +
+           " and event expected " + stateExpected);
+      }
+    };
+  });
 
-  peer.onsignalingstatechange = function(e) {
-    info(peer + ": 'signalingstatechange' event received");
-    var state = e.target.signalingState;
-    if(stateExpected === state && !eventFired) {
-      eventFired = true;
-      peer.setRemoteDescStableEventDate = new Date();
-      check_next_test();
-    } else {
-      ok(false, "This event has either already fired or there has been a " +
-                "mismatch between event received " + state +
-                " and event expected " + stateExpected);
-    }
-  };
+  var stateChanged = peer.setRemoteDescription(desc).then(() => {
+    peer.setRemoteDescDate = new Date();
+  });
 
-  peer.setRemoteDescription(desc, function () {
-    stateChanged = true;
-    peer.setRemoteDescDate = new Date();
-    check_next_test();
-  });
+  return Promise.all([eventFired, stateChanged]);
 };
 
 /**
  * Start running the tests as assigned to the command chain.
  */
-PeerConnectionTest.prototype.run = function PCT_run() {
-  this.next();
-};
-
-/**
- * Clean up the objects used by the test
- */
-PeerConnectionTest.prototype.teardown = function PCT_teardown() {
-  this.close(function () {
-    info("Test finished");
-    if (window.SimpleTest)
-      networkTestFinished();
-    else
-      finish();
-  });
+PeerConnectionTest.prototype.run = function() {
+  return this.chain.execute()
+    .then(() => this.close())
+    .then(() => {
+      if (window.SimpleTest) {
+        networkTestFinished();
+      } else {
+        finish();
+      }
+    })
+    .catch(e =>
+           ok(false, 'Error in test execution: ' + e +
+              ((typeof e.stack === 'string') ?
+               (' ' + e.stack.split('\n').join(' ... ')) : '')));
 };
 
 /**
  * Routes ice candidates from one PCW to the other PCW
  */
-PeerConnectionTest.prototype.iceCandidateHandler = function
-PCT_iceCandidateHandler(caller, candidate) {
-  var self = this;
-
+PeerConnectionTest.prototype.iceCandidateHandler = function(caller, candidate) {
   info("Received: " + JSON.stringify(candidate) + " from " + caller);
 
   var target = null;
   if (caller.contains("pcLocal")) {
-    if (self.pcRemote) {
-      target = self.pcRemote;
+    if (this.pcRemote) {
+      target = this.pcRemote;
     }
   } else if (caller.contains("pcRemote")) {
-    if (self.pcLocal) {
-      target = self.pcLocal;
+    if (this.pcLocal) {
+      target = this.pcLocal;
     }
   } else {
     ok(false, "received event from unknown caller: " + caller);
     return;
   }
 
   if (target) {
     target.storeOrAddIceCandidate(candidate);
@@ -1205,110 +519,83 @@ PCT_iceCandidateHandler(caller, candidat
     send_message({"type": "ice_candidate", "ice_candidate": candidate});
   }
 };
 
 /**
  * Installs a polling function for the socket.io client to read
  * all messages from the chat room into a message queue.
  */
-PeerConnectionTest.prototype.setupSignalingClient = function
-PCT_setupSignalingClient() {
-  var self = this;
+PeerConnectionTest.prototype.setupSignalingClient = function() {
+  this.signalingMessageQueue = [];
+  this.signalingCallbacks = {};
+  this.signalingLoopRun = true;
 
-  self.signalingMessageQueue = [];
-  self.signalingCallbacks = {};
-  self.signalingLoopRun = true;
-
-  function queueMessage(message) {
+  var queueMessage = message => {
     info("Received signaling message: " + JSON.stringify(message));
     var fired = false;
-    Object.keys(self.signalingCallbacks).forEach(function(name) {
+    Object.keys(this.signalingCallbacks).forEach(name => {
       if (name === message.type) {
         info("Invoking callback for message type: " + name);
-        self.signalingCallbacks[name](message);
+        this.signalingCallbacks[name](message);
         fired = true;
       }
     });
     if (!fired) {
-      self.signalingMessageQueue.push(message);
-      info("signalingMessageQueue.length: " + self.signalingMessageQueue.length);
+      this.signalingMessageQueue.push(message);
+      info("signalingMessageQueue.length: " + this.signalingMessageQueue.length);
     }
-    if (self.signalingLoopRun) {
+    if (this.signalingLoopRun) {
       wait_for_message().then(queueMessage);
     } else {
       info("Exiting signaling message event loop");
     }
-  }
-
+  };
   wait_for_message().then(queueMessage);
 }
 
 /**
  * Sets a flag to stop reading further messages from the chat room.
  */
-PeerConnectionTest.prototype.signalingMessagesFinished = function
-PCT_signalingMessagesFinished() {
+PeerConnectionTest.prototype.signalingMessagesFinished = function() {
   this.signalingLoopRun = false;
 }
 
 /**
- * Callback to stop reading message from chat room once trickle ICE
- * on the far end is over.
- *
- * @param {string} caller
- *        The lable of the caller of the function
- */
-PeerConnectionTest.prototype.signalEndOfTrickleIce = function
-PCT_signalEndOfTrickleIce(caller) {
-  if (this.steeplechase) {
-    send_message({"type": "end_of_trickle_ice"});
-  }
-};
-
-/**
  * Register a callback function to deliver messages from the chat room
  * directly instead of storing them in the message queue.
  *
  * @param {string} messageType
  *        For which message types should the callback get invoked.
  *
  * @param {function} onMessage
  *        The function which gets invoked if a message of the messageType
  *        has been received from the chat room.
  */
-PeerConnectionTest.prototype.registerSignalingCallback = function
-PCT_registerSignalingCallback(messageType, onMessage) {
+PeerConnectionTest.prototype.registerSignalingCallback = function(messageType, onMessage) {
   this.signalingCallbacks[messageType] = onMessage;
-}
+};
 
 /**
  * Searches the message queue for the first message of a given type
  * and invokes the given callback function, or registers the callback
  * function for future messages if the queue contains no such message.
  *
  * @param {string} messageType
  *        The type of message to search and register for.
- *
- * @param {function} onMessage
- *        The callback function which gets invoked with the messages
- *        of the given mesage type.
  */
-PeerConnectionTest.prototype.getSignalingMessage = function
-PCT_getSignalingMessage(messageType, onMessage) {
-  for(var i=0; i < this.signalingMessageQueue.length; i++) {
-    if (messageType === this.signalingMessageQueue[i].type) {
-      //FIXME
-      info("invoking callback on message " + i + " from message queue, for message type:" + messageType);
-      onMessage(this.signalingMessageQueue.splice(i, 1)[0]);
-      return;
-    }
+PeerConnectionTest.prototype.getSignalingMessage = function(messageType) {
+    var i = this.signalingMessageQueue.findIndex(m => m.type === messageType);
+  if (i >= 0) {
+    info("invoking callback on message " + i + " from message queue, for message type:" + messageType);
+    return Promise.resolve(this.signalingMessageQueue.splice(i, 1)[0]);
   }
-  this.registerSignalingCallback(messageType, onMessage);
-}
+  return new Promise(resolve =>
+                     this.registerSignalingCallback(messageType, resolve));
+};
 
 
 /**
  * This class acts as a wrapper around a DataChannel instance.
  *
  * @param dataChannel
  * @param peerConnectionWrapper
  * @constructor
@@ -1317,62 +604,27 @@ function DataChannelWrapper(dataChannel,
   this._channel = dataChannel;
   this._pc = peerConnectionWrapper;
 
   info("Creating " + this);
 
   /**
    * Setup appropriate callbacks
    */
-
-  this.onclose = unexpectedEventAndFinish(this, 'onclose');
-  this.onerror = unexpectedEventAndFinish(this, 'onerror');
-  this.onmessage = unexpectedEventAndFinish(this, 'onmessage');
-  this.onopen = unexpectedEventAndFinish(this, 'onopen');
-
-  var self = this;
-
-  /**
-   * Callback for native data channel 'onclose' events. If no custom handler
-   * has been specified via 'this.onclose', a failure will be raised if an
-   * event of this type gets caught.
-   */
-  this._channel.onclose = function () {
-    info(self + ": 'onclose' event fired");
-
-    self.onclose(self);
-    self.onclose = unexpectedEventAndFinish(self, 'onclose');
-  };
+  createOneShotEventWrapper(this, this._channel, 'close');
+  createOneShotEventWrapper(this, this._channel, 'error');
+  createOneShotEventWrapper(this, this._channel, 'message');
 
-  /**
-   * Callback for native data channel 'onmessage' events. If no custom handler
-   * has been specified via 'this.onmessage', a failure will be raised if an
-   * event of this type gets caught.
-   *
-   * @param {Object} event
-   *        Event data which includes the sent message
-   */
-  this._channel.onmessage = function (event) {
-    info(self + ": 'onmessage' event fired for '" + event.data + "'");
-
-    self.onmessage(event.data);
-    self.onmessage = unexpectedEventAndFinish(self, 'onmessage');
-  };
-
-  /**
-   * Callback for native data channel 'onopen' events. If no custom handler
-   * has been specified via 'this.onopen', a failure will be raised if an
-   * event of this type gets caught.
-   */
-  this._channel.onopen = function () {
-    info(self + ": 'onopen' event fired");
-
-    self.onopen(self);
-    self.onopen = unexpectedEventAndFinish(self, 'onopen');
-  };
+  this.opened = timerGuard(new Promise(resolve => {
+    this._channel.onopen = () => {
+      this._channel.onopen = unexpectedEvent(this, 'onopen');
+      is(this.readyState, "open", "data channel is 'open' after 'onopen'");
+      resolve(this);
+    };
+  }), 60000, "channel didn't open in time");
 }
 
 DataChannelWrapper.prototype = {
   /**
    * Returns the binary type of the channel
    *
    * @returns {String} The binary type
    */
@@ -1446,27 +698,27 @@ DataChannelWrapper.prototype = {
   },
 
   /**
    * Send data through the data channel
    *
    * @param {String|Object} data
    *        Data which has to be sent through the data channel
    */
-  send: function DCW_send(data) {
+  send: function(data) {
     info(this + ": Sending data '" + data + "'");
     this._channel.send(data);
   },
 
   /**
    * Returns the string representation of the class
    *
    * @returns {String} The string representation
    */
-  toString: function DCW_toString() {
+  toString: function() {
     return "DataChannelWrapper (" + this._pc.label + '_' + this._channel.label + ")";
   }
 };
 
 
 /**
  * This class acts as a wrapper around a PeerConnection instance.
  *
@@ -1483,119 +735,73 @@ function PeerConnectionWrapper(label, co
 
   this.constraints = [ ];
   this.offerOptions = {};
   this.streams = [ ];
   this.mediaCheckers = [ ];
 
   this.dataChannels = [ ];
 
-  this.onAddStreamAudioCounter = 0;
-  this.onAddStreamVideoCounter = 0;
-  this.addStreamCallbacks = {};
+  this.addStreamCounter = {audio: 0, video: 0 };
 
   this._local_ice_candidates = [];
   this._remote_ice_candidates = [];
-  this._ice_candidates_to_add = [];
-  this.holdIceCandidates = true;
-  this.endOfTrickleIce = false;
+  this.holdIceCandidates = new Promise(r => this.releaseIceCandidates = r);
   this.localRequiresTrickleIce = false;
-  this.remoteRequiresTrickleIce  = false;
+  this.remoteRequiresTrickleIce = false;
+  this.localMediaElements = [];
 
   this.h264 = typeof h264 !== "undefined" ? true : false;
 
   info("Creating " + this);
   this._pc = new mozRTCPeerConnection(this.configuration);
 
   /**
    * Setup callback handlers
    */
-  var self = this;
-  // This enables tests to validate that the next ice state is the one they expect to happen
-  this.next_ice_state = ""; // in most cases, the next state will be "checking", but in some tests "closed"
   // This allows test to register their own callbacks for ICE connection state changes
   this.ice_connection_callbacks = {};
 
-  this._pc.oniceconnectionstatechange = function() {
-    ok(self._pc.iceConnectionState !== undefined, "iceConnectionState should not be undefined");
-    info(self + ": oniceconnectionstatechange fired, new state is: " + self._pc.iceConnectionState);
-    Object.keys(self.ice_connection_callbacks).forEach(function(name) {
-      self.ice_connection_callbacks[name]();
+  this._pc.oniceconnectionstatechange = e => {
+    isnot(typeof this._pc.iceConnectionState, "undefined",
+          "iceConnectionState should not be undefined");
+    info(this + ": oniceconnectionstatechange fired, new state is: " + this._pc.iceConnectionState);
+    Object.keys(this.ice_connection_callbacks).forEach(name => {
+      this.ice_connection_callbacks[name]();
     });
-    if (self.next_ice_state !== "") {
-      is(self._pc.iceConnectionState, self.next_ice_state, "iceConnectionState changed to '" +
-         self.next_ice_state + "'");
-      self.next_ice_state = "";
-    }
   };
 
   /**
    * Callback for native peer connection 'onaddstream' events.
    *
    * @param {Object} event
    *        Event data which includes the stream to be added
    */
-  this._pc.onaddstream = function (event) {
-    info(self + ": 'onaddstream' event fired for " + JSON.stringify(event.stream));
+  this._pc.onaddstream = event => {
+    info(this + ": 'onaddstream' event fired for " + JSON.stringify(event.stream));
 
     var type = '';
     if (event.stream.getAudioTracks().length > 0) {
       type = 'audio';
-      self.onAddStreamAudioCounter += event.stream.getAudioTracks().length;
+      this.addStreamCounter.audio += this.countTracksInStreams('audio', [event.stream]);
     }
     if (event.stream.getVideoTracks().length > 0) {
       type += 'video';
-      self.onAddStreamVideoCounter += event.stream.getVideoTracks().length;
+      this.addStreamCounter.video += this.countTracksInStreams('video', [event.stream]);
     }
-    self.attachMedia(event.stream, type, 'remote');
-
-    Object.keys(self.addStreamCallbacks).forEach(function(name) {
-      info(self + " calling addStreamCallback " + name);
-      self.addStreamCallbacks[name]();
-    });
-   };
-
-  this.ondatachannel = unexpectedEventAndFinish(this, 'ondatachannel');
-
-  /**
-   * Callback for native peer connection 'ondatachannel' events. If no custom handler
-   * has been specified via 'this.ondatachannel', a failure will be raised if an
-   * event of this type gets caught.
-   *
-   * @param {Object} event
-   *        Event data which includes the newly created data channel
-   */
-  this._pc.ondatachannel = function (event) {
-    info(self + ": 'ondatachannel' event fired for " + event.channel.label);
-
-    self.ondatachannel(new DataChannelWrapper(event.channel, self));
-    self.ondatachannel = unexpectedEventAndFinish(self, 'ondatachannel');
+    this.attachMedia(event.stream, type, 'remote');
   };
 
-  this.onsignalingstatechange = unexpectedEventAndFinish(this, 'onsignalingstatechange');
-  this.signalingStateCallbacks = {};
+  createOneShotEventWrapper(this, this._pc, 'datachannel');
+  this._pc.addEventListener('datachannel', e => {
+    var wrapper = new DataChannelWrapper(e.channel, this);
+    this.dataChannels.push(wrapper);
+  });
 
-  /**
-   * Callback for native peer connection 'onsignalingstatechange' events. If no
-   * custom handler has been specified via 'this.onsignalingstatechange', a
-   * failure will be raised if an event of this type is caught.
-   *
-   * @param {Object} aEvent
-   */
-  this._pc.onsignalingstatechange = function (anEvent) {
-    info(self + ": 'onsignalingstatechange' event fired");
-
-    Object.keys(self.signalingStateCallbacks).forEach(function(name) {
-      self.signalingStateCallbacks[name](anEvent);
-    });
-    // this calls the eventhandler only once and then overwrites it with the
-    // default unexpectedEvent handler
-    self.onsignalingstatechange(anEvent);
-    self.onsignalingstatechange = unexpectedEventAndFinish(self, 'onsignalingstatechange');
-  };
+  createOneShotEventWrapper(this, this._pc, 'signalingstatechange');
 }
 
 PeerConnectionWrapper.prototype = {
 
   /**
    * Returns the local description.
    *
    * @returns {object} The local description
@@ -1660,489 +866,380 @@ PeerConnectionWrapper.prototype = {
    *
    * @param {MediaStream} stream
    *        Media stream to handle
    * @param {string} type
    *        The type of media stream ('audio' or 'video')
    * @param {string} side
    *        The location the stream is coming from ('local' or 'remote')
    */
-  attachMedia : function PCW_attachMedia(stream, type, side) {
-    function isSenderOfTrack(sender) {
-      return sender.track == this;
-    }
-
+  attachMedia : function(stream, type, side) {
     info("Got media stream: " + type + " (" + side + ")");
     this.streams.push(stream);
 
     if (side === 'local') {
       // In order to test both the addStream and addTrack APIs, we do video one
       // way and audio + audiovideo the other.
       if (type == "video") {
         this._pc.addStream(stream);
-        ok(this._pc.getSenders().find(isSenderOfTrack,
-                                      stream.getVideoTracks()[0]),
+        ok(this._pc.getSenders().find(sender => sender.track == stream.getVideoTracks()[0]),
            "addStream adds sender");
       } else {
-        stream.getTracks().forEach(function(track) {
+        stream.getTracks().forEach(track => {
           var sender = this._pc.addTrack(track, stream);
           is(sender.track, track, "addTrack returns sender");
-        }.bind(this));
+        });
       }
     }
 
     var element = createMediaElement(type, this.label + '_' + side + this.streams.length);
     this.mediaCheckers.push(new MediaElementChecker(element));
     element.mozSrcObject = stream;
     element.play();
+
+    // Store local media elements so that we can stop them when done.
+    // Don't store remote ones because they should stop when the PC does.
+    if (side === 'local') {
+      this.localMediaElements.push(element);
+    }
   },
 
   /**
    * Requests all the media streams as specified in the constrains property.
    *
-   * @param {function} onSuccess
-   *        Callback to execute if all media has been requested successfully
    * @param {array} constraintsList
    *        Array of constraints for GUM calls
    */
-  getAllUserMedia : function PCW_GetAllUserMedia(constraintsList, onSuccess) {
-    var self = this;
-
-    function _getAllUserMedia(index) {
-      if (index < constraintsList.length) {
-        var constraints = constraintsList[index];
-
-        getUserMedia(constraints, function (stream) {
-          var type = '';
-
-          if (constraints.audio) {
-            type = 'audio';
-          }
-
-          if (constraints.video) {
-            type += 'video';
-          }
-
-          self.attachMedia(stream, type, 'local');
-
-          _getAllUserMedia(index + 1);
-        }, generateErrorCallback());
-      } else {
-        onSuccess();
-      }
+  getAllUserMedia : function(constraintsList) {
+    if (constraintsList.length === 0) {
+      info("Skipping GUM: no UserMedia requested");
+      return Promise.resolve();
     }
 
-    if (constraintsList.length === 0) {
-      info("Skipping GUM: no UserMedia requested");
-      onSuccess();
-    }
-    else {
-      info("Get " + constraintsList.length + " local streams");
-      _getAllUserMedia(0);
-    }
+    info("Get " + constraintsList.length + " local streams");
+    return Promise.all(constraintsList.map(constraints => {
+      return getUserMedia(constraints).then(stream => {
+        var type = '';
+        if (constraints.audio) {
+          type = 'audio';
+        }
+        if (constraints.video) {
+          type += 'video';
+        }
+        this.attachMedia(stream, type, 'local');
+      });
+    }));
+  },
+
+  /**
+   * Create a new data channel instance.  Also creates a promise called
+   * `this.nextDataChannel` that resolves when the next data channel arrives.
+   */
+  expectDataChannel: function(message) {
+    this.nextDataChannel = new Promise(resolve => {
+      this.ondatachannel = e => {
+        ok(e.channel, message);
+        resolve(e.channel);
+      };
+    });
   },
 
   /**
    * Create a new data channel instance
    *
    * @param {Object} options
    *        Options which get forwarded to nsIPeerConnection.createDataChannel
-   * @param {function} [onCreation=undefined]
-   *        Callback to execute when the local data channel has been created
    * @returns {DataChannelWrapper} The created data channel
    */
-  createDataChannel : function PCW_createDataChannel(options, onCreation) {
+  createDataChannel : function(options) {
     var label = 'channel_' + this.dataChannels.length;
     info(this + ": Create data channel '" + label);
 
     var channel = this._pc.createDataChannel(label, options);
     var wrapper = new DataChannelWrapper(channel, this);
-
-    if (onCreation) {
-      wrapper.onopen = function () {
-        onCreation(wrapper);
-      };
-    }
-
     this.dataChannels.push(wrapper);
     return wrapper;
   },
 
   /**
    * Creates an offer and automatically handles the failure case.
    *
    * @param {function} onSuccess
    *        Callback to execute if the offer was created successfully
    */
-  createOffer : function PCW_createOffer(onSuccess) {
-    var self = this;
-
-    this._pc.createOffer(function (offer) {
+  createOffer : function() {
+    return this._pc.createOffer(this.offerOptions).then(offer => {
       info("Got offer: " + JSON.stringify(offer));
       // note: this might get updated through ICE gathering
-      self._latest_offer = offer;
-      if (self.h264) {
+      this._latest_offer = offer;
+      if (this.h264) {
         isnot(offer.sdp.search("H264/90000"), -1, "H.264 should be present in the SDP offer");
         offer.sdp = removeVP8(offer.sdp);
       }
-      onSuccess(offer);
-    }, generateErrorCallback(), this.offerOptions);
+      return offer;
+    });
   },
 
   /**
    * Creates an answer and automatically handles the failure case.
-   *
-   * @param {function} onSuccess
-   *        Callback to execute if the answer was created successfully
    */
-  createAnswer : function PCW_createAnswer(onSuccess) {
-    var self = this;
-
-    this._pc.createAnswer(function (answer) {
-      info(self + ": Got answer: " + JSON.stringify(answer));
-      self._last_answer = answer;
-      onSuccess(answer);
-    }, generateErrorCallback());
+  createAnswer : function() {
+    return this._pc.createAnswer().then(answer => {
+      info(this + ": Got answer: " + JSON.stringify(answer));
+      this._last_answer = answer;
+      return answer;
+    });
   },
 
   /**
    * Sets the local description and automatically handles the failure case.
    *
    * @param {object} desc
    *        mozRTCSessionDescription for the local description request
-   * @param {function} onSuccess
-   *        Callback to execute if the local description was set successfully
    */
-  setLocalDescription : function PCW_setLocalDescription(desc, onSuccess) {
-    var self = this;
-
-    if (onSuccess) {
-      this._pc.setLocalDescription(desc, function () {
-        info(self + ": Successfully set the local description");
-        onSuccess();
-      }, generateErrorCallback());
-    } else {
-      this._pc.setLocalDescription(desc);
-    }
+  setLocalDescription : function(desc) {
+    return this._pc.setLocalDescription(desc).then(() => {
+      info(this + ": Successfully set the local description");
+    });
   },
 
   /**
    * Tries to set the local description and expect failure. Automatically
    * causes the test case to fail if the call succeeds.
    *
    * @param {object} desc
    *        mozRTCSessionDescription for the local description request
-   * @param {function} onFailure
-   *        Callback to execute if the call fails.
+   * @returns {Promise}
+   *        A promise that resolves to the expected error
    */
-  setLocalDescriptionAndFail : function PCW_setLocalDescriptionAndFail(desc, onFailure) {
-    var self = this;
-    this._pc.setLocalDescription(desc,
+  setLocalDescriptionAndFail : function(desc) {
+    return this._pc.setLocalDescription(desc).then(
       generateErrorCallback("setLocalDescription should have failed."),
-      function (err) {
-        info(self + ": As expected, failed to set the local description");
-        onFailure(err);
-    });
+      err => {
+        info(this + ": As expected, failed to set the local description");
+        return err;
+      });
   },
 
   /**
    * Sets the remote description and automatically handles the failure case.
    *
    * @param {object} desc
    *        mozRTCSessionDescription for the remote description request
-   * @param {function} onSuccess
-   *        Callback to execute if the remote description was set successfully
    */
-  setRemoteDescription : function PCW_setRemoteDescription(desc, onSuccess) {
-    var self = this;
-
-    if (!onSuccess) {
-      this._pc.setRemoteDescription(desc);
-      this.addStoredIceCandidates();
-      return;
-    }
-    this._pc.setRemoteDescription(desc, function () {
-      info(self + ": Successfully set remote description");
-      self.addStoredIceCandidates();
-      onSuccess();
-    }, generateErrorCallback());
+  setRemoteDescription : function(desc) {
+    return this._pc.setRemoteDescription(desc).then(() => {
+      info(this + ": Successfully set remote description");
+      this.releaseIceCandidates();
+    });
   },
 
   /**
    * Tries to set the remote description and expect failure. Automatically
    * causes the test case to fail if the call succeeds.
    *
    * @param {object} desc
    *        mozRTCSessionDescription for the remote description request
-   * @param {function} onFailure
-   *        Callback to execute if the call fails.
+   * @returns {Promise}
+   *        a promise that resolve to the returned error
    */
-  setRemoteDescriptionAndFail : function PCW_setRemoteDescriptionAndFail(desc, onFailure) {
-    var self = this;
-    this._pc.setRemoteDescription(desc,
+  setRemoteDescriptionAndFail : function(desc) {
+    return this._pc.setRemoteDescription(desc).then(
       generateErrorCallback("setRemoteDescription should have failed."),
-      function (err) {
-        info(self + ": As expected, failed to set the remote description");
-        onFailure(err);
+      err => {
+        info(this + ": As expected, failed to set the remote description");
+        return err;
     });
   },
 
   /**
    * Registers a callback for the signaling state change and
    * appends the new state to an array for logging it later.
    */
-  logSignalingState: function PCW_logSignalingState() {
-    var self = this;
-
-    function _logSignalingState(e) {
-      var newstate = self._pc.signalingState;
-      var oldstate = self.signalingStateLog[self.signalingStateLog.length - 1]
-      if (Object.keys(signalingStateTransitions).indexOf(oldstate) != -1) {
-        ok(signalingStateTransitions[oldstate].indexOf(newstate) != -1, self + ": legal signaling state transition from " + oldstate + " to " + newstate);
+  logSignalingState: function() {
+    this.signalingStateLog = [this._pc.signalingState];
+    this._pc.addEventListener('signalingstatechange', e => {
+      var newstate = this._pc.signalingState;
+      var oldstate = this.signalingStateLog[this.signalingStateLog.length - 1]
+      if (Object.keys(signalingStateTransitions).indexOf(oldstate) >= 0) {
+        ok(signalingStateTransitions[oldstate].indexOf(newstate) >= 0, this + ": legal signaling state transition from " + oldstate + " to " + newstate);
       } else {
-        ok(false, self + ": old signaling state " + oldstate + " missing in signaling transition array");
+        ok(false, this + ": old signaling state " + oldstate + " missing in signaling transition array");
       }
-      self.signalingStateLog.push(newstate);
-    }
-
-    self.signalingStateLog = [self._pc.signalingState];
-    self.signalingStateCallbacks.logSignalingStatus = _logSignalingState;
+      this.signalingStateLog.push(newstate);
+    });
   },
 
   /**
    * Either adds a given ICE candidate right away or stores it to be added
    * later, depending on the state of the PeerConnection.
    *
    * @param {object} candidate
    *        The mozRTCIceCandidate to be added or stored
    */
-  storeOrAddIceCandidate : function PCW_storeOrAddIceCandidate(candidate) {
-    var self = this;
-
-    self._remote_ice_candidates.push(candidate);
-    if (self.signalingState === 'closed') {
+  storeOrAddIceCandidate : function(candidate) {
+    this._remote_ice_candidates.push(candidate);
+    if (this.signalingState === 'closed') {
       info("Received ICE candidate for closed PeerConnection - discarding");
       return;
     }
-    if (!self.holdIceCandidates) {
-      self.addIceCandidate(candidate);
-    } else {
-      self._ice_candidates_to_add.push(candidate);
-    }
-  },
-
-  addStoredIceCandidates : function PCW_addStoredIceCandidates() {
-    var self = this;
-
-    self.holdIceCandidates = false;
-    if ((self._ice_candidates_to_add) &&
-        (self._ice_candidates_to_add.length > 0)) {
-      info("adding stored ice candidates");
-      for (var i = 0; i < self._ice_candidates_to_add.length; i++) {
-        self.addIceCandidate(self._ice_candidates_to_add[i]);
-      }
-      self._ice_candidates_to_add = [];
-    }
+    this.holdIceCandidates.then(() => {
+      this.addIceCandidate(candidate);
+    });
   },
 
   /**
    * Adds an ICE candidate and automatically handles the failure case.
    *
    * @param {object} candidate
    *        SDP candidate
-   * @param {function} onSuccess
-   *        Callback to execute if the local description was set successfully
    */
-  addIceCandidate : function PCW_addIceCandidate(candidate, onSuccess) {
-    var self = this;
-
-    info(self + ": adding ICE candidate " + JSON.stringify(candidate));
-    this._pc.addIceCandidate(candidate, function () {
-      info(self + ": Successfully added an ICE candidate");
-      if (onSuccess) {
-        onSuccess();
-      }
-    }, generateErrorCallback());
-  },
-
-  /**
-   * Tries to add an ICE candidate and expects failure. Automatically
-   * causes the test case to fail if the call succeeds.
-   *
-   * @param {object} candidate
-   *        SDP candidate
-   * @param {function} onFailure
-   *        Callback to execute if the call fails.
-   */
-  addIceCandidateAndFail : function PCW_addIceCandidateAndFail(candidate, onFailure) {
-    var self = this;
-
-    this._pc.addIceCandidate(candidate,
-      generateErrorCallback("addIceCandidate should have failed."),
-      function (err) {
-        info(self + ": As expected, failed to add an ICE candidate");
-        onFailure(err);
-    }) ;
+  addIceCandidate : function(candidate) {
+    info(this + ": adding ICE candidate " + JSON.stringify(candidate));
+    return this._pc.addIceCandidate(candidate).then(() => {
+      info(this + ": Successfully added an ICE candidate");
+    });
   },
 
   /**
    * Returns if the ICE the connection state is "connected".
    *
    * @returns {boolean} True if the connection state is "connected", otherwise false.
    */
-  isIceConnected : function PCW_isIceConnected() {
+  isIceConnected : function() {
     info(this + ": iceConnectionState = " + this.iceConnectionState);
     return this.iceConnectionState === "connected";
   },
 
   /**
    * Returns if the ICE the connection state is "checking".
    *
    * @returns {boolean} True if the connection state is "checking", otherwise false.
    */
-  isIceChecking : function PCW_isIceChecking() {
+  isIceChecking : function() {
     return this.iceConnectionState === "checking";
   },
 
   /**
    * Returns if the ICE the connection state is "new".
    *
    * @returns {boolean} True if the connection state is "new", otherwise false.
    */
-  isIceNew : function PCW_isIceNew() {
+  isIceNew : function() {
     return this.iceConnectionState === "new";
   },
 
   /**
    * Checks if the ICE connection state still waits for a connection to get
    * established.
    *
    * @returns {boolean} True if the connection state is "checking" or "new",
    *  otherwise false.
    */
-  isIceConnectionPending : function PCW_isIceConnectionPending() {
+  isIceConnectionPending : function() {
     return (this.isIceChecking() || this.isIceNew());
   },
 
   /**
    * Registers a callback for the ICE connection state change and
    * appends the new state to an array for logging it later.
    */
-  logIceConnectionState: function PCW_logIceConnectionState() {
-    var self = this;
-
-    function logIceConState () {
-      var newstate = self._pc.iceConnectionState;
-      var oldstate = self.iceConnectionLog[self.iceConnectionLog.length - 1]
+  logIceConnectionState: function() {
+    this.iceConnectionLog = [this._pc.iceConnectionState];
+    this.ice_connection_callbacks.logIceStatus = () => {
+      var newstate = this._pc.iceConnectionState;
+      var oldstate = this.iceConnectionLog[this.iceConnectionLog.length - 1]
       if (Object.keys(iceStateTransitions).indexOf(oldstate) != -1) {
-        ok(iceStateTransitions[oldstate].indexOf(newstate) != -1, self + ": legal ICE state transition from " + oldstate + " to " + newstate);
+        ok(iceStateTransitions[oldstate].indexOf(newstate) != -1, this + ": legal ICE state transition from " + oldstate + " to " + newstate);
       } else {
-        ok(false, self + ": old ICE state " + oldstate + " missing in ICE transition array");
+        ok(false, this + ": old ICE state " + oldstate + " missing in ICE transition array");
       }
-      self.iceConnectionLog.push(newstate);
-    }
-
-    self.iceConnectionLog = [self._pc.iceConnectionState];
-    self.ice_connection_callbacks.logIceStatus = logIceConState;
+      this.iceConnectionLog.push(newstate);
+    };
   },
 
   /**
    * Registers a callback for the ICE connection state change and
    * reports success (=connected) or failure via the callbacks.
    * States "new" and "checking" are ignored.
    *
-   * @param {function} onSuccess
-   *        Callback if ICE connection status is "connected".
-   * @param {function} onFailure
-   *        Callback if ICE connection reaches a different state than
-   *        "new", "checking" or "connected".
+   * @returns {Promise}
+   *          resolves when connected, rejects on failure
    */
-  waitForIceConnected : function PCW_waitForIceConnected(onSuccess, onFailure) {
-    var self = this;
-    var mySuccess = onSuccess;
-    var myFailure = onFailure;
+  waitForIceConnected : function() {
+    return new Promise((resolve, reject) => {
+      var iceConnectedChanged = () => {
+        if (this.isIceConnected()) {
+          delete this.ice_connection_callbacks.waitForIceConnected;
+          resolve();
+        } else if (! this.isIceConnectionPending()) {
+          delete this.ice_connection_callbacks.waitForIceConnected;
+          resolve();
+        }
+      }
 
-    function iceConnectedChanged () {
-      if (self.isIceConnected()) {
-        delete self.ice_connection_callbacks.waitForIceConnected;
-        mySuccess();
-      } else if (! self.isIceConnectionPending()) {
-        delete self.ice_connection_callbacks.waitForIceConnected;
-        myFailure();
-      }
-    }
-
-    self.ice_connection_callbacks.waitForIceConnected = iceConnectedChanged;
+      this.ice_connection_callbacks.waitForIceConnected = iceConnectedChanged;
+    });
   },
 
   /**
    * Setup a onicecandidate handler
    *
    * @param {object} test
    *        A PeerConnectionTest object to which the ice candidates gets
    *        forwarded.
    */
-  setupIceCandidateHandler : function
-    PCW_setupIceCandidateHandler(test, candidateHandler, endHandler) {
-    var self = this;
-
+  setupIceCandidateHandler : function(test, candidateHandler) {
     candidateHandler = candidateHandler || test.iceCandidateHandler.bind(test);
-    endHandler = endHandler || test.signalEndOfTrickleIce.bind(test);
+
+    var resolveEndOfTrickle;
+    this.endOfTrickleIce = new Promise(r => resolveEndOfTrickle = r);
 
-    function iceCandidateCallback (anEvent) {
-      info(self.label + ": received iceCandidateEvent");
+    this.endOfTrickleIce.then(() => {
+      this._pc.onicecandidate = () =>
+        ok(false, this.label + " received ICE candidate after end of trickle");
+    });
+
+    this._pc.onicecandidate = anEvent => {
       if (!anEvent.candidate) {
-        info(self.label + ": received end of trickle ICE event");
-        self.endOfTrickleIce = true;
-        endHandler(self.label);
-      } else {
-        if (self.endOfTrickleIce) {
-          ok(false, "received ICE candidate after end of trickle");
-        }
-        info(self.label + ": iceCandidate = " + JSON.stringify(anEvent.candidate));
-        ok(anEvent.candidate.candidate.length > 0, "ICE candidate contains candidate");
-        // we don't support SDP MID's yet
-        ok(anEvent.candidate.sdpMid.length === 0, "SDP MID has length zero");
-        ok(typeof anEvent.candidate.sdpMLineIndex === 'number', "SDP MLine Index needs to exist");
-        self._local_ice_candidates.push(anEvent.candidate);
-        candidateHandler(self.label, anEvent.candidate);
+        info(this.label + ": received end of trickle ICE event");
+        resolveEndOfTrickle(this.label);
+        return;
       }
-    }
 
-    self._pc.onicecandidate = iceCandidateCallback;
+      info(this.label + ": iceCandidate = " + JSON.stringify(anEvent.candidate));
+      ok(anEvent.candidate.candidate.length > 0, "ICE candidate contains candidate");
+      // we don't support SDP MID's yet
+      ok(anEvent.candidate.sdpMid.length === 0, "SDP MID has length zero");
+      ok(typeof anEvent.candidate.sdpMLineIndex === 'number', "SDP MLine Index needs to exist");
+      this._local_ice_candidates.push(anEvent.candidate);
+      candidateHandler(this.label, anEvent.candidate);
+    };
   },
 
   /**
    * Counts the amount of audio tracks in a given media constraint.
    *
    * @param constraints
    *        The contraint to be examined.
    */
-  countAudioTracksInMediaConstraint : function
-    PCW_countAudioTracksInMediaConstraint(constraints) {
-    if ((!constraints) || (constraints.length === 0)) {
+  countTracksInConstraint : function(type, constraints) {
+    if (!Array.isArray(constraints)) {
       return 0;
     }
-    var numAudioTracks = 0;
-    for (var i = 0; i < constraints.length; i++) {
-      if (constraints[i].audio) {
-        numAudioTracks++;
-      }
-    }
-    return numAudioTracks;
+    return constraints.reduce((sum, c) => sum + (c[type] ? 1 : 0), 0);
   },
 
   /**
    * Checks for audio in given offer options.
    *
    * @param options
    *        The options to be examined.
    */
-  audioInOfferOptions : function
-    PCW_audioInOfferOptions(options) {
+  audioInOfferOptions : function(options) {
     if (!options) {
       return 0;
     }
 
     var offerToReceiveAudio = options.offerToReceiveAudio;
 
     // TODO: Remove tests of old constraint-like RTCOptions soon (Bug 1064223).
     if (options.mandatory && options.mandatory.OfferToReceiveAudio !== undefined) {
@@ -2155,43 +1252,22 @@ PeerConnectionWrapper.prototype = {
     if (offerToReceiveAudio) {
       return 1;
     } else {
       return 0;
     }
   },
 
   /**
-   * Counts the amount of video tracks in a given media constraint.
-   *
-   * @param constraint
-   *        The contraint to be examined.
-   */
-  countVideoTracksInMediaConstraint : function
-    PCW_countVideoTracksInMediaConstraint(constraints) {
-    if ((!constraints) || (constraints.length === 0)) {
-      return 0;
-    }
-    var numVideoTracks = 0;
-    for (var i = 0; i < constraints.length; i++) {
-      if (constraints[i].video) {
-        numVideoTracks++;
-      }
-    }
-    return numVideoTracks;
-  },
-
-  /**
    * Checks for video in given offer options.
    *
    * @param options
    *        The options to be examined.
    */
-  videoInOfferOptions : function
-    PCW_videoInOfferOptions(options) {
+  videoInOfferOptions : function(options) {
     if (!options) {
       return 0;
     }
 
     var offerToReceiveVideo = options.offerToReceiveVideo;
 
     // TODO: Remove tests of old constraint-like RTCOptions soon (Bug 1064223).
     if (options.mandatory && options.mandatory.OfferToReceiveVideo !== undefined) {
@@ -2204,189 +1280,140 @@ PeerConnectionWrapper.prototype = {
     if (offerToReceiveVideo) {
       return 1;
     } else {
       return 0;
     }
   },
 
   /*
-   * Counts the amount of audio tracks in a given set of streams.
+   * Counts the amount of tracks of the given type in a set of streams.
    *
+   * @param type audio|video
    * @param streams
    *        An array of streams (as returned by getLocalStreams()) to be
    *        examined.
    */
-  countAudioTracksInStreams : function PCW_countAudioTracksInStreams(streams) {
-    if (!streams || (streams.length === 0)) {
+  countTracksInStreams: function(type, streams) {
+    if (!Array.isArray(streams)) {
       return 0;
     }
-    var numAudioTracks = 0;
-    streams.forEach(function(st) {
-      numAudioTracks += st.getAudioTracks().length;
-    });
-    return numAudioTracks;
-  },
+    var f = (type === 'video') ? "getVideoTracks" : "getAudioTracks";
 
-  /*
-   * Counts the amount of video tracks in a given set of streams.
-   *
-   * @param streams
-   *        An array of streams (as returned by getLocalStreams()) to be
-   *        examined.
-   */
-  countVideoTracksInStreams: function PCW_countVideoTracksInStreams(streams) {
-    if (!streams || (streams.length === 0)) {
-      return 0;
-    }
-    var numVideoTracks = 0;
-    streams.forEach(function(st) {
-      numVideoTracks += st.getVideoTracks().length;
-    });
-    return numVideoTracks;
+    return streams.reduce((count, st) => {
+      return count + st[f]().length;
+    }, 0);
   },
 
   /**
    * Checks that we are getting the media tracks we expect.
    *
-   * @param {object} constraintsRemote
-   *        The media constraints of the local and remote peer connection object
+   * @param {object} constraints
+   *        The media constraints of the remote peer connection object
    */
-  checkMediaTracks : function PCW_checkMediaTracks(constraintsRemote, onSuccess) {
-    var self = this;
-
-    function _checkMediaTracks(constraintsRemote, onSuccess) {
-
-      var localConstraintAudioTracks =
-        self.countAudioTracksInMediaConstraint(self.constraints);
-      var localStreams = self._pc.getLocalStreams();
-      var localAudioTracks = self.countAudioTracksInStreams(localStreams, false);
-      is(localAudioTracks, localConstraintAudioTracks, self + ' has ' +
-        localAudioTracks + ' local audio tracks');
-
-      var localConstraintVideoTracks =
-        self.countVideoTracksInMediaConstraint(self.constraints);
-      var localVideoTracks = self.countVideoTracksInStreams(localStreams, false);
-      is(localVideoTracks, localConstraintVideoTracks, self + ' has ' +
-        localVideoTracks + ' local video tracks');
-
-      var remoteConstraintAudioTracks =
-        self.countAudioTracksInMediaConstraint(constraintsRemote);
-      var remoteStreams = self._pc.getRemoteStreams();
-      var remoteAudioTracks = self.countAudioTracksInStreams(remoteStreams, false);
-      is(remoteAudioTracks, remoteConstraintAudioTracks, self + ' has ' +
-        remoteAudioTracks + ' remote audio tracks');
-
-      var remoteConstraintVideoTracks =
-        self.countVideoTracksInMediaConstraint(constraintsRemote);
-      var remoteVideoTracks = self.countVideoTracksInStreams(remoteStreams, false);
-      is(remoteVideoTracks, remoteConstraintVideoTracks, self + ' has ' +
-        remoteVideoTracks + ' remote video tracks');
+  checkMediaTracks : function(remoteConstraints) {
+    var waitForExpectedTracks = type => {
+      var outstandingCount = this.countTracksInConstraint(type, remoteConstraints);
+      outstandingCount -= this.addStreamCounter[type];
+      if (outstandingCount <= 0) {
+        return Promise.resolve();
+      }
 
-      onSuccess();
-    }
-
-    // we have to do this check as the onaddstream never fires if the remote
-    // stream has no track at all!
-    var expectedRemoteTracks =
-      self.countAudioTracksInMediaConstraint(constraintsRemote) +
-      self.countVideoTracksInMediaConstraint(constraintsRemote);
-
-    // TODO: this whole counting of streams should be replaced with comparing
-    //       media stream objects IDs and what we got in the SDP (bug 1089798)
-    function _compareReceivedAndExpectedTracks(constraintsRemote, onSuccess) {
-      var receivedRemoteTracks =
-        self.onAddStreamAudioCounter + self.onAddStreamVideoCounter;
+      return new Promise(resolve => {
+        this._pc.addEventListener('addstream', e => {
+          outstandingCount -= this.countTracksInStreams(type, [e.stream]);
+          if (outstandingCount <= 0) {
+            resolve();
+          }
+        });
+      });
+    };
 
-      if (receivedRemoteTracks === expectedRemoteTracks) {
-        _checkMediaTracks(constraintsRemote, onSuccess);
-      } else if (receivedRemoteTracks > expectedRemoteTracks) {
-        ok(false, "Received more streams " + receivedRemoteTracks +
-            " then expected " + expectedRemoteTracks);
-        _checkMediaTracks(constraintsRemote, onSuccess);
-      } else {
-        info("Still waiting for more remote streams to arrive (" +
-            receivedRemoteTracks + " vs " + expectedRemoteTracks + ")");
-      }
-    }
+    var checkTrackCounts = (side, streams, constraints) => {
+      ['audio', 'video'].forEach(type => {
+        var actual = this.countTracksInStreams(type, streams);
+        var expected = this.countTracksInConstraint(type, constraints);
+        is(actual, expected, this + ' has ' + actual + ' ' +
+           side + ' ' + type + ' tracks');
+      });
+    };
 
-    if (expectedRemoteTracks > (self.onAddStreamAudioCounter +
-        self.onAddStreamVideoCounter)) {
-      // This installs a callback handler for every time onaddstrem fires.
-      // We rely on the outer mochitest timeout to catch the case where
-      // onaddstream never fires
-      self.addStreamCallbacks.checkMediaTracks = function() {
-        _compareReceivedAndExpectedTracks(constraintsRemote, onSuccess);
-      };
-    }
-    _compareReceivedAndExpectedTracks(constraintsRemote, onSuccess);
-
+    info(this + " checkMediaTracks() got called before onAddStream fired");
+    var checkPromise = Promise.all([
+      waitForExpectedTracks('audio'),
+      waitForExpectedTracks('video')
+    ]).then(() => {
+      checkTrackCounts('local', this._pc.getLocalStreams(), this.constraints);
+      checkTrackCounts('remote', this._pc.getRemoteStreams(), remoteConstraints);
+    });
+    return timerGuard(checkPromise, 60000, "onaddstream never fired");
   },
 
-  checkMsids : function PCW_checkMsids() {
-    function _checkMsids(desc, streams, sdpLabel) {
-      streams.forEach(function(stream) {
-        stream.getTracks().forEach(function(track) {
+  checkMsids: function() {
+    var checkSdpForMsids = (desc, streams, side) => {
+      streams.forEach(stream => {
+        stream.getTracks().forEach(track => {
           // TODO(bug 1089798): Once DOMMediaStream has an id field, we
           // should be verifying that the SDP contains
           // a=msid:<stream-id> <track-id>
           ok(desc.sdp.match(new RegExp("a=msid:[^ ]+ " + track.id)),
-             sdpLabel + " SDP contains track id " + track.id );
+             side + " SDP contains track id " + track.id );
         });
       });
-    }
+    };
 
-    _checkMsids(this.localDescription,
-                this._pc.getLocalStreams(),
-                "local");
-    _checkMsids(this.remoteDescription,
-                this._pc.getRemoteStreams(),
-                "remote");
+    checkSdpForMsids(this.localDescription, this._pc.getLocalStreams(),
+                     "local");
+    checkSdpForMsids(this.remoteDescription, this._pc.getRemoteStreams(),
+                     "remote");
   },
 
-  verifySdp : function PCW_verifySdp(desc, expectedType, offerConstraintsList,
-      offerOptions, trickleIceCallback) {
+  verifySdp: function(desc, expectedType, offerConstraintsList, offerOptions, isLocal) {
     info("Examining this SessionDescription: " + JSON.stringify(desc));
     info("offerConstraintsList: " + JSON.stringify(offerConstraintsList));
     info("offerOptions: " + JSON.stringify(offerOptions));
     ok(desc, "SessionDescription is not null");
     is(desc.type, expectedType, "SessionDescription type is " + expectedType);
     ok(desc.sdp.length > 10, "SessionDescription body length is plausible");
     ok(desc.sdp.contains("a=ice-ufrag"), "ICE username is present in SDP");
     ok(desc.sdp.contains("a=ice-pwd"), "ICE password is present in SDP");
     ok(desc.sdp.contains("a=fingerprint"), "ICE fingerprint is present in SDP");
     //TODO: update this for loopback support bug 1027350
     ok(!desc.sdp.contains(LOOPBACK_ADDR), "loopback interface is absent from SDP");
-    if (desc.sdp.contains("a=candidate")) {
-      ok(true, "at least one ICE candidate is present in SDP");
-      trickleIceCallback(false);
+    var requiresTrickleIce = !desc.sdp.contains("a=candidate");
+    if (requiresTrickleIce) {
+      info("at least one ICE candidate is present in SDP");
     } else {
       info("No ICE candidate in SDP -> requiring trickle ICE");
-      trickleIceCallback(true);
     }
+    if (isLocal) {
+      this.localRequiresTrickleIce = requiresTrickleIce;
+    } else {
+      this.remoteRequiresTrickleIce = requiresTrickleIce;
+    }
+
     //TODO: how can we check for absence/presence of m=application?
 
     var audioTracks =
-      this.countAudioTracksInMediaConstraint(offerConstraintsList) ||
+        this.countTracksInConstraint('audio', offerConstraintsList) ||
       this.audioInOfferOptions(offerOptions);
 
     info("expected audio tracks: " + audioTracks);
     if (audioTracks == 0) {
       ok(!desc.sdp.contains("m=audio"), "audio m-line is absent from SDP");
     } else {
       ok(desc.sdp.contains("m=audio"), "audio m-line is present in SDP");
       ok(desc.sdp.contains("a=rtpmap:109 opus/48000/2"), "OPUS codec is present in SDP");
       //TODO: ideally the rtcp-mux should be for the m=audio, and not just
       //      anywhere in the SDP (JS SDP parser bug 1045429)
       ok(desc.sdp.contains("a=rtcp-mux"), "RTCP Mux is offered in SDP");
-
     }
 
     var videoTracks =
-      this.countVideoTracksInMediaConstraint(offerConstraintsList) ||
+        this.countTracksInConstraint('video', offerConstraintsList) ||
       this.videoInOfferOptions(offerOptions);
 
     info("expected video tracks: " + videoTracks);
     if (videoTracks == 0) {
       ok(!desc.sdp.contains("m=video"), "video m-line is absent from SDP");
     } else {
       ok(desc.sdp.contains("m=video"), "video m-line is present in SDP");
       if (this.h264) {
@@ -2397,69 +1424,45 @@ PeerConnectionWrapper.prototype = {
       ok(desc.sdp.contains("a=rtcp-mux"), "RTCP Mux is offered in SDP");
     }
 
   },
 
   /**
    * Check that media flow is present on all media elements involved in this
    * test by waiting for confirmation that media flow is present.
-   *
-   * @param {Function} onSuccess the success callback when media flow
-   *                             is confirmed on all media elements
    */
-  checkMediaFlowPresent : function PCW_checkMediaFlowPresent(onSuccess) {
-    var self = this;
-
-    function _checkMediaFlowPresent(index, onSuccess) {
-      if(index >= self.mediaCheckers.length) {
-        onSuccess();
-      } else {
-        var mediaChecker = self.mediaCheckers[index];
-        mediaChecker.waitForMediaFlow(function() {
-          _checkMediaFlowPresent(index + 1, onSuccess);
-        });
-      }
-    }
-
-    _checkMediaFlowPresent(0, onSuccess);
+  checkMediaFlowPresent : function() {
+    return Promise.all(this.mediaCheckers.map(checker => checker.waitForMediaFlow()));
   },
 
   /**
    * Check that stats are present by checking for known stats.
-   *
-   * @param {Function} onSuccess the success callback to return stats to
    */
-  getStats : function PCW_getStats(selector, onSuccess) {
-    var self = this;
-
-    this._pc.getStats(selector, function(stats) {
-      info(self + ": Got stats: " + JSON.stringify(stats));
-      self._last_stats = stats;
-      onSuccess(stats);
-    }, generateErrorCallback());
+  getStats : function(selector) {
+    return this._pc.getStats(selector).then(stats => {
+      info(this + ": Got stats: " + JSON.stringify(stats));
+      this._last_stats = stats;
+      return stats;
+    });
   },
 
   /**
    * Checks that we are getting the media streams we expect.
    *
    * @param {object} stats
    *        The stats to check from this PeerConnectionWrapper
    */
-  checkStats : function PCW_checkStats(stats, twoMachines) {
-    function toNum(obj) {
-      return obj? obj : 0;
-    }
-    function numTracks(streams) {
-      var n = 0;
-      streams.forEach(function(stream) {
-          n += stream.getAudioTracks().length + stream.getVideoTracks().length;
-        });
-      return n;
-    }
+  checkStats : function(stats, twoMachines) {
+    var toNum = obj => obj? obj : 0;
+    var numTracks = streams =>
+        streams.reduce((count, stream) => count +
+                       stream.getAudioTracks().length +
+                       stream.getVideoTracks().length,
+                       0);
 
     const isWinXP = navigator.userAgent.indexOf("Windows NT 5.1") != -1;
 
     // Use spec way of enumerating stats
     var counters = {};
     for (var key in stats) {
       if (stats.hasOwnProperty(key)) {
         var res = stats[key];
@@ -2534,17 +1537,17 @@ PeerConnectionWrapper.prototype = {
             break;
           }
         }
       }
     }
 
     // Use MapClass way of enumerating stats
     var counters2 = {};
-    stats.forEach(function(res) {
+    stats.forEach(res => {
         if (!res.isRemote) {
           counters2[res.type] = toNum(counters2[res.type]) + 1;
         }
       });
     is(JSON.stringify(counters), JSON.stringify(counters2),
        "Spec and MapClass variant of RTCStatsReport enumeration agree");
     var nin = numTracks(this._pc.getRemoteStreams());
     var nout = numTracks(this._pc.getLocalStreams());
@@ -2570,21 +1573,20 @@ PeerConnectionWrapper.prototype = {
 
   /**
    * Compares the Ice server configured for this PeerConnectionWrapper
    * with the ICE candidates received in the RTCP stats.
    *
    * @param {object} stats
    *        The stats to be verified for relayed vs. direct connection.
    */
-  checkStatsIceConnectionType : function PCW_checkStatsIceConnectionType(stats)
-  {
+  checkStatsIceConnectionType : function(stats) {
     var lId;
     var rId;
-    Object.keys(stats).forEach(function(name) {
+    Object.keys(stats).forEach(name => {
       if ((stats[name].type === "candidatepair") &&
           (stats[name].selected)) {
         lId = stats[name].localCandidateId;
         rId = stats[name].remoteCandidateId;
       }
     });
     info("checkStatsIceConnectionType verifying: local=" +
          JSON.stringify(stats[lId]) + " remote=" + JSON.stringify(stats[rId]));
@@ -2617,36 +1619,36 @@ PeerConnectionWrapper.prototype = {
    *
    * @param {object} stats
    *        The stats to check for ICE candidate pairs
    * @param {object} counters
    *        The counters for media and data tracks based on constraints
    * @param {object} answer
    *        The SDP answer to check for SDP bundle support
    */
-  checkStatsIceConnections : function PCW_checkStatsIceConnections(stats,
+  checkStatsIceConnections : function(stats,
       offerConstraintsList, offerOptions, answer) {
     var numIceConnections = 0;
-    Object.keys(stats).forEach(function(key) {
+    Object.keys(stats).forEach(key => {
       if ((stats[key].type === "candidatepair") && stats[key].selected) {
         numIceConnections += 1;
       }
     });
     info("ICE connections according to stats: " + numIceConnections);
     if (answer.sdp.contains('a=group:BUNDLE')) {
       is(numIceConnections, 1, "stats reports exactly 1 ICE connection");
     } else {
       // This code assumes that no media sections have been rejected due to
       // codec mismatch or other unrecoverable negotiation failures.
       var numAudioTracks =
-        this.countAudioTracksInMediaConstraint(offerConstraintsList) ||
+          this.countTracksInConstraint('audio', offerConstraintsList) ||
         this.audioInOfferOptions(offerOptions);
 
       var numVideoTracks =
-        this.countVideoTracksInMediaConstraint(offerConstraintsList) ||
+          this.countTracksInConstraint('video', offerConstraintsList) ||
         this.videoInOfferOptions(offerOptions);
 
       var numDataTracks = this.dataChannels.length;
 
       var numAudioVideoDataTracks = numAudioTracks + numVideoTracks + numDataTracks;
       info("expected audio + video + data tracks: " + numAudioVideoDataTracks);
       is(numAudioVideoDataTracks, numIceConnections, "stats ICE connections matches expected A/V tracks");
     }
@@ -2656,17 +1658,17 @@ PeerConnectionWrapper.prototype = {
    * Property-matching function for finding a certain stat in passed-in stats
    *
    * @param {object} stats
    *        The stats to check from this PeerConnectionWrapper
    * @param {object} props
    *        The properties to look for
    * @returns {boolean} Whether an entry containing all match-props was found.
    */
-  hasStat : function PCW_hasStat(stats, props) {
+  hasStat : function(stats, props) {
     for (var key in stats) {
       if (stats.hasOwnProperty(key)) {
         var res = stats[key];
         var match = true;
         for (var prop in props) {
           if (res[prop] !== props[prop]) {
             match = false;
             break;
@@ -2678,39 +1680,53 @@ PeerConnectionWrapper.prototype = {
       }
     }
     return false;
   },
 
   /**
    * Closes the connection
    */
-  close : function PCW_close() {
-    this._ice_candidates_to_add = [];
+  close : function() {
     this._pc.close();
+    this.localMediaElements.forEach(e => e.pause());
     info(this + ": Closed connection.");
   },
 
   /**
-   * Register all events during the setup of the data channel
-   *
-   * @param {Function} onDataChannelOpened
-   *        Callback to execute when the data channel has been opened
-   */
-  registerDataChannelOpenEvents : function (onDataChannelOpened) {
-    info(this + ": Register callback for 'ondatachannel'");
-
-    this.ondatachannel = function (targetChannel) {
-      this.dataChannels.push(targetChannel);
-      info(this + ": 'ondatachannel' fired, registering 'onopen' callback");
-      targetChannel.onopen = onDataChannelOpened;
-    };
-  },
-
-  /**
    * Returns the string representation of the class
    *
    * @returns {String} The string representation
    */
-  toString : function PCW_toString() {
+  toString : function() {
     return "PeerConnectionWrapper (" + this.label + ")";
   }
 };
+
+// haxx to prevent SimpleTest from failing at window.onload
+function addLoadEvent() {}
+
+var scriptsReady = Promise.all([
+  "/tests/SimpleTest/SimpleTest.js",
+  "head.js",
+  "templates.js",
+  "turnConfig.js",
+  "dataChannel.js",
+  "network.js"
+].map(script  => {
+  var el = document.createElement("script");
+  if (typeof scriptRelativePath === 'string' && script.charAt(0) !== '/') {
+    script = scriptRelativePath + script;
+  }
+  el.src = script;
+  document.head.appendChild(el);
+  return new Promise(r => { el.onload = r; el.onerror = r; });
+}));
+
+function createHTML(options) {
+  return scriptsReady.then(() => realCreateHTML(options));
+}
+
+function runNetworkTest(testFunction) {
+  return scriptsReady
+    .then(() => startNetworkAndTest())
+    .then(() => runTestWhenReady(testFunction));
+}
--- a/dom/media/tests/mochitest/steeplechase.ini
+++ b/dom/media/tests/mochitest/steeplechase.ini
@@ -1,9 +1,10 @@
 [DEFAULT]
 support-files =
   head.js
   mediaStreamPlayback.js
+  network.js
   pc.js
   templates.js
   turnConfig.js
 
 [test_peerConnection_basicAudio.html]
--- a/dom/media/tests/mochitest/templates.js
+++ b/dom/media/tests/mochitest/templates.js
@@ -57,682 +57,414 @@ function dumpSdp(test) {
   if ((test.pcLocal) && (test.pcRemote) &&
     (typeof test.pcRemote.setLocalDescDate !== 'undefined') &&
     (typeof test.pcRemote.setLocalDescStableEventDate !== 'undefined')) {
     var delta = deltaSeconds(test.pcRemote.setLocalDescDate, test.pcRemote.setLocalDescStableEventDate);
     dump("Delay between pcRemote.setLocal <-> pcRemote.signalingStateStable: " + delta + "\n");
   }
 }
 
-var commandsPeerConnection = [
-  [
-    'PC_SETUP_SIGNALING_CLIENT',
-    function (test) {
-      if (test.steeplechase) {
-        test.setTimeout(30000);
-        test.setupSignalingClient();
-        test.registerSignalingCallback("ice_candidate", function (message) {
-          var pc = test.pcRemote ? test.pcRemote : test.pcLocal;
-          pc.storeOrAddIceCandidate(new mozRTCIceCandidate(message.ice_candidate));
-        });
-        test.registerSignalingCallback("end_of_trickle_ice", function (message) {
-          test.signalingMessagesFinished();
-        });
-      }
-      test.next();
-    }
-  ],
-  [
-    'PC_LOCAL_SETUP_ICE_LOGGER',
-    function (test) {
-      test.pcLocal.logIceConnectionState();
-      test.next();
+function waitForIceConnected(test, pc) {
+  if (pc.isIceConnected()) {
+    info(pc + ": ICE connection state log: " + pc.iceConnectionLog);
+    ok(true, pc + ": ICE is in connected state");
+    return Promise.resolve();
+  }
+
+  if (!pc.isIceConnectionPending()) {
+    dumpSdp(test);
+    var details = pc + ": ICE is already in bad state: " + pc.iceConnectionState;
+    ok(false, details);
+    return Promise.reject(new Error(details));
+  }
+
+  return pc.waitForIceConnected()
+    .then(() => {
+      info(pc + ": ICE connection state log: " + pc.iceConnectionLog);
+      ok(pc.isIceConnected(), pc + ": ICE switched to 'connected' state");
+    });
+}
+
+// We need to verify that at least one candidate has been (or will be) gathered.
+function waitForAnIceCandidate(pc) {
+  return new Promise(resolve => {
+    if (!pc.localRequiresTrickleIce ||
+        pc._local_ice_candidates.length > 0) {
+      resolve();
+    } else {
+      // In some circumstances, especially when both PCs are on the same
+      // browser, even though we are connected, the connection can be
+      // established without receiving a single candidate from one or other
+      // peer.  So we wait for at least one...
+      pc._pc.addEventListener('icecandidate', resolve);
     }
-  ],
-  [
-    'PC_REMOTE_SETUP_ICE_LOGGER',
-    function (test) {
-      test.pcRemote.logIceConnectionState();
-      test.next();
-    }
-  ],
-  [
-    'PC_LOCAL_SETUP_SIGNALING_LOGGER',
-    function (test) {
-      test.pcLocal.logSignalingState();
-      test.next();
-    }
-  ],
-  [
-    'PC_REMOTE_SETUP_SIGNALING_LOGGER',
-    function (test) {
-      test.pcRemote.logSignalingState();
-      test.next();
-    }
-  ],
-  [
-    'PC_LOCAL_GUM',
-    function (test) {
-      test.pcLocal.getAllUserMedia(test.pcLocal.constraints, function () {
-        test.next();
+  }).then(() => {
+    ok(pc._local_ice_candidates.length > 0,
+       pc + " received local trickle ICE candidates");
+    isnot(pc._pc.iceGatheringState, GATH_NEW,
+          pc + " ICE gathering state is not 'new'");
+  });
+}
+
+function checkTrackStats(pc, audio, outbound) {
+  var stream = outbound ? pc._pc.getLocalStreams()[0] : pc._pc.getRemoteStreams()[0];
+  if (!stream) {
+    return Promise.resolve();
+  }
+  var track = audio ? stream.getAudioTracks()[0] : stream.getVideoTracks()[0];
+  if (!track) {
+    return Promise.resolve();
+  }
+  var msg = pc + " stats " + (outbound ? "outbound " : "inbound ") +
+      (audio ? "audio" : "video") + " rtp ";
+  return pc.getStats(track).then(stats => {
+    ok(pc.hasStat(stats, {
+      type: outbound ? "outboundrtp" : "inboundrtp",
+      isRemote: false,
+      mediaType: audio ? "audio" : "video"
+    }), msg + "1");
+    ok(!pc.hasStat(stats, {
+      type: outbound ? "inboundrtp" : "outboundrtp",
+      isRemote: false
+    }), msg + "2");
+    ok(!pc.hasStat(stats, {
+      mediaType: audio ? "video" : "audio"
+    }), msg + "3");
+  });
+}
+
+// checks all stats combinations inbound/outbound, audio/video
+var checkAllTrackStats = pc =>
+    Promise.all([0, 1, 2, 3].map(i => checkTrackStats(pc, i & 1, i & 2)));
+
+var commandsPeerConnection = [
+  function PC_SETUP_SIGNALING_CLIENT(test) {
+    if (test.steeplechase) {
+      setTimeout(() => {
+        ok(false, "PeerConnectionTest timed out");
+        test.teardown();
+      }, 30000);
+      test.setupSignalingClient();
+      test.registerSignalingCallback("ice_candidate", function (message) {
+        var pc = test.pcRemote ? test.pcRemote : test.pcLocal;
+        pc.storeOrAddIceCandidate(new mozRTCIceCandidate(message.ice_candidate));
       });
-    }
-  ],
-  [
-    'PC_REMOTE_GUM',
-    function (test) {
-      test.pcRemote.getAllUserMedia(test.pcRemote.constraints, function () {
-        test.next();
+      test.registerSignalingCallback("end_of_trickle_ice", function (message) {
+        test.signalingMessagesFinished();
       });
     }
-  ],
-  [
-    'PC_LOCAL_CHECK_INITIAL_SIGNALINGSTATE',
-    function (test) {
-      is(test.pcLocal.signalingState, STABLE,
-         "Initial local signalingState is 'stable'");
-      test.next();
-    }
-  ],
-  [
-    'PC_REMOTE_CHECK_INITIAL_SIGNALINGSTATE',
-    function (test) {
-      is(test.pcRemote.signalingState, STABLE,
-         "Initial remote signalingState is 'stable'");
-      test.next();
-    }
-  ],
-  [
-    'PC_LOCAL_CHECK_INITIAL_ICE_STATE',
-    function (test) {
-      is(test.pcLocal.iceConnectionState, ICE_NEW,
-        "Initial local ICE connection state is 'new'");
-      test.next();
+  },
+
+  function PC_LOCAL_SETUP_ICE_LOGGER(test) {
+    test.pcLocal.logIceConnectionState();
+  },
+
+  function PC_REMOTE_SETUP_ICE_LOGGER(test) {
+    test.pcRemote.logIceConnectionState();
+  },
+
+  function PC_LOCAL_SETUP_SIGNALING_LOGGER(test) {
+    test.pcLocal.logSignalingState();
+  },
+
+  function PC_REMOTE_SETUP_SIGNALING_LOGGER(test) {
+    test.pcRemote.logSignalingState();
+  },
+
+  function PC_LOCAL_GUM(test) {
+    return test.pcLocal.getAllUserMedia(test.pcLocal.constraints);
+  },
+
+  function PC_REMOTE_GUM(test) {
+    return test.pcRemote.getAllUserMedia(test.pcRemote.constraints);
+  },
+
+  function PC_LOCAL_CHECK_INITIAL_SIGNALINGSTATE(test) {
+    is(test.pcLocal.signalingState, STABLE,
+       "Initial local signalingState is 'stable'");
+  },
+
+  function PC_REMOTE_CHECK_INITIAL_SIGNALINGSTATE(test) {
+    is(test.pcRemote.signalingState, STABLE,
+       "Initial remote signalingState is 'stable'");
+  },
+
+  function PC_LOCAL_CHECK_INITIAL_ICE_STATE(test) {
+    is(test.pcLocal.iceConnectionState, ICE_NEW,
+       "Initial local ICE connection state is 'new'");
+  },
+
+  function PC_REMOTE_CHECK_INITIAL_ICE_STATE(test) {
+    is(test.pcRemote.iceConnectionState, ICE_NEW,
+       "Initial remote ICE connection state is 'new'");
+  },
+
+  function PC_LOCAL_SETUP_ICE_HANDLER(test) {
+    test.pcLocal.setupIceCandidateHandler(test);
+    if (test.steeplechase) {
+      test.pcLocal.endOfTrickleIce.then(() => {
+        send_message({"type": "end_of_trickle_ice"});
+      });
     }
-  ],
-  [
-    'PC_REMOTE_CHECK_INITIAL_ICE_STATE',
-    function (test) {
-      is(test.pcRemote.iceConnectionState, ICE_NEW,
-        "Initial remote ICE connection state is 'new'");
-      test.next();
-    }
-  ],
-  [
-    'PC_LOCAL_SETUP_ICE_HANDLER',
-    function (test) {
-      test.pcLocal.setupIceCandidateHandler(test);
-      test.next();
-    }
-  ],
-  [
-    'PC_REMOTE_SETUP_ICE_HANDLER',
-    function (test) {
-      test.pcRemote.setupIceCandidateHandler(test);
-      test.next();
-    }
-  ],
-  [
-    'PC_LOCAL_CREATE_OFFER',
-    function (test) {
-      test.createOffer(test.pcLocal, function (offer) {
-        is(test.pcLocal.signalingState, STABLE,
-           "Local create offer does not change signaling state");
-        test.next();
+  },
+
+  function PC_REMOTE_SETUP_ICE_HANDLER(test) {
+    test.pcRemote.setupIceCandidateHandler(test);
+    if (test.steeplechase) {
+      test.pcRemote.endOfTrickleIce.then(() => {
+        send_message({"type": "end_of_trickle_ice"});
       });
     }
-  ],
-  [
-    'PC_LOCAL_STEEPLECHASE_SIGNAL_OFFER',
-    function (test) {
-      if (test.steeplechase) {
-        send_message({"type": "offer",
-          "offer": test.originalOffer,
-          "offer_constraints": test.pcLocal.constraints,
-          "offer_options": test.pcLocal.offerOptions});
-        test._local_offer = test.originalOffer;
-        test._offer_constraints = test.pcLocal.constraints;
-        test._offer_options = test.pcLocal.offerOptions;
-      }
-      test.next();
+  },
+
+  function PC_LOCAL_CREATE_OFFER(test) {
+    return test.createOffer(test.pcLocal).then(offer => {
+      is(test.pcLocal.signalingState, STABLE,
+         "Local create offer does not change signaling state");
+    });
+  },
+
+  function PC_LOCAL_STEEPLECHASE_SIGNAL_OFFER(test) {
+    if (test.steeplechase) {
+      send_message({"type": "offer",
+                    "offer": test.originalOffer,
+                    "offer_constraints": test.pcLocal.constraints,
+                    "offer_options": test.pcLocal.offerOptions});
+      test._local_offer = test.originalOffer;
+      test._offer_constraints = test.pcLocal.constraints;
+      test._offer_options = test.pcLocal.offerOptions;
     }
-  ],
-  [
-    'PC_LOCAL_SET_LOCAL_DESCRIPTION',
-    function (test) {
-      test.setLocalDescription(test.pcLocal, test.originalOffer, HAVE_LOCAL_OFFER, function () {
+  },
+
+  function PC_LOCAL_SET_LOCAL_DESCRIPTION(test) {
+    return test.setLocalDescription(test.pcLocal, test.originalOffer, HAVE_LOCAL_OFFER)
+      .then(() => {
         is(test.pcLocal.signalingState, HAVE_LOCAL_OFFER,
            "signalingState after local setLocalDescription is 'have-local-offer'");
-        test.next();
       });
+  },
+
+  function PC_REMOTE_GET_OFFER(test) {
+    if (!test.steeplechase) {
+      test._local_offer = test.originalOffer;
+      test._offer_constraints = test.pcLocal.constraints;
+      test._offer_options = test.pcLocal.offerOptions;
+      return Promise.resolve();
     }
-  ],
-  [
-    'PC_REMOTE_GET_OFFER',
-    function (test) {
-      if (!test.steeplechase) {
-        test._local_offer = test.originalOffer;
-        test._offer_constraints = test.pcLocal.constraints;
-        test._offer_options = test.pcLocal.offerOptions;
-        test.next();
-      } else {
-        test.getSignalingMessage("offer", function (message) {
-          ok("offer" in message, "Got an offer message");
-          test._local_offer = new mozRTCSessionDescription(message.offer);
-          test._offer_constraints = message.offer_constraints;
-          test._offer_options = message.offer_options;
-          test.next();
-        });
-      }
-    }
-  ],
-  [
-    'PC_REMOTE_SET_REMOTE_DESCRIPTION',
-    function (test) {
-      test.setRemoteDescription(test.pcRemote, test._local_offer, HAVE_REMOTE_OFFER, function () {
+    return test.getSignalingMessage("offer")
+      .then(message => {
+        ok("offer" in message, "Got an offer message");
+        test._local_offer = new mozRTCSessionDescription(message.offer);
+        test._offer_constraints = message.offer_constraints;
+        test._offer_options = message.offer_options;
+      });
+  },
+
+  function PC_REMOTE_SET_REMOTE_DESCRIPTION(test) {
+    return test.setRemoteDescription(test.pcRemote, test._local_offer, HAVE_REMOTE_OFFER)
+      .then(() => {
         is(test.pcRemote.signalingState, HAVE_REMOTE_OFFER,
            "signalingState after remote setRemoteDescription is 'have-remote-offer'");
-        test.next();
       });
-    }
-  ],
-  [
-    'PC_LOCAL_SANE_LOCAL_SDP',
-    function (test) {
-      test.pcLocal.verifySdp(test._local_offer, "offer",
-        test._offer_constraints, test._offer_options,
-        function(trickle) {
-          test.pcLocal.localRequiresTrickleIce = trickle;
-        });
-      test.next();
-    }
-  ],
-  [
-    'PC_REMOTE_SANE_REMOTE_SDP',
-    function (test) {
-      test.pcRemote.verifySdp(test._local_offer, "offer",
-        test._offer_constraints, test._offer_options,
-        function (trickle) {
-          test.pcRemote.remoteRequiresTrickleIce = trickle;
-        });
-      test.next();
-    }
-  ],
-  [
-    'PC_REMOTE_CREATE_ANSWER',
-    function (test) {
-      test.createAnswer(test.pcRemote, function (answer) {
+  },
+
+  function PC_LOCAL_SANE_LOCAL_SDP(test) {
+    test.pcLocal.verifySdp(test._local_offer, "offer",
+                           test._offer_constraints, test._offer_options,
+                           true);
+  },
+
+  function PC_REMOTE_SANE_REMOTE_SDP(test) {
+    test.pcRemote.verifySdp(test._local_offer, "offer",
+                            test._offer_constraints, test._offer_options,
+                            false);
+  },
+
+  function PC_REMOTE_CREATE_ANSWER(test) {
+    return test.createAnswer(test.pcRemote)
+      .then(answer => {
         is(test.pcRemote.signalingState, HAVE_REMOTE_OFFER,
            "Remote createAnswer does not change signaling state");
         if (test.steeplechase) {
           send_message({"type": "answer",
                         "answer": test.originalAnswer,
                         "answer_constraints": test.pcRemote.constraints});
           test._remote_answer = test.pcRemote._last_answer;
           test._answer_constraints = test.pcRemote.constraints;
         }
-        test.next();
       });
-    }
-  ],
-  [
-    'PC_REMOTE_CHECK_FOR_DUPLICATED_PORTS_IN_SDP',
-    function (test) {
-      var re = /a=candidate.* (UDP|TCP) [\d]+ ([\d\.]+) ([\d]+) typ host/g;
+  },
+
+  function PC_REMOTE_CHECK_FOR_DUPLICATED_PORTS_IN_SDP(test) {
+    var re = /a=candidate.* (UDP|TCP) [\d]+ ([\d\.]+) ([\d]+) typ host/g;
 
-      function _sdpCandidatesIntoArray(sdp) {
-        var regexArray = [];
-        var resultArray = [];
-        while ((regexArray = re.exec(sdp)) !== null) {
-          info("regexArray: " + regexArray);
-          if ((regexArray[1] === "TCP") && (regexArray[3] === "9")) {
-            // As both sides can advertise TCP active connection on port 9 lets
-            // ignore them all together
-            info("Ignoring TCP candidate on port 9");
-            continue;
-          }
-          const triple = regexArray[1] + ":" + regexArray[2] + ":" + regexArray[3];
-          info("triple: " + triple);
-          if (resultArray.indexOf(triple) !== -1) {
-            dump("SDP: " + sdp.replace(/[\r]/g, '') + "\n");
-            ok(false, "This Transport:IP:Port " + triple + " appears twice in the SDP above!");
-          }
-          resultArray.push(triple);
-        }
-        return resultArray;
-      }
-
-      const offerTriples = _sdpCandidatesIntoArray(test._local_offer.sdp);
-      info("Offer ICE host candidates: " + JSON.stringify(offerTriples));
-
-      const answerTriples = _sdpCandidatesIntoArray(test.originalAnswer.sdp);
-      info("Answer ICE host candidates: " + JSON.stringify(answerTriples));
-
-      for (var i=0; i< offerTriples.length; i++) {
-        if (answerTriples.indexOf(offerTriples[i]) !== -1) {
-          dump("SDP offer: " + test._local_offer.sdp.replace(/[\r]/g, '') + "\n");
-          dump("SDP answer: " + test.originalAnswer.sdp.replace(/[\r]/g, '') + "\n");
-          ok(false, "This IP:Port " + offerTriples[i] + " appears in SDP offer and answer!");
+    var _sdpCandidatesIntoArray = sdp => {
+      var regexArray = [];
+      var resultArray = [];
+      while ((regexArray = re.exec(sdp)) !== null) {
+        info("regexArray: " + regexArray);
+        if ((regexArray[1] === "TCP") && (regexArray[3] === "9")) {
+          // As both sides can advertise TCP active connection on port 9 lets
+          // ignore them all together
+          info("Ignoring TCP candidate on port 9");
+          continue;
         }
-      }
-
-      test.next();
-    }
-  ],
-  [
-    'PC_REMOTE_SET_LOCAL_DESCRIPTION',
-    function (test) {
-      test.setLocalDescription(test.pcRemote, test.originalAnswer, STABLE,
-        function () {
-          is(test.pcRemote.signalingState, STABLE,
-            "signalingState after remote setLocalDescription is 'stable'");
-          test.next();
-        }
-      );
-    }
-  ],
-  [
-    'PC_LOCAL_GET_ANSWER',
-    function (test) {
-      if (!test.steeplechase) {
-        test._remote_answer = test.originalAnswer;
-        test._answer_constraints = test.pcRemote.constraints;
-        test.next();
-      } else {
-        test.getSignalingMessage("answer", function (message) {
-          ok("answer" in message, "Got an answer message");
-          test._remote_answer = new mozRTCSessionDescription(message.answer);
-          test._answer_constraints = message.answer_constraints;
-          test.next();
-        });
-      }
-    }
-  ],
-  [
-    'PC_LOCAL_SET_REMOTE_DESCRIPTION',
-    function (test) {
-      test.setRemoteDescription(test.pcLocal, test._remote_answer, STABLE,
-        function () {
-          is(test.pcLocal.signalingState, STABLE,
-            "signalingState after local setRemoteDescription is 'stable'");
-          test.next();
+        var triple = regexArray[1] + ":" + regexArray[2] + ":" + regexArray[3];
+        info("triple: " + triple);
+        if (resultArray.indexOf(triple) !== -1) {
+          dump("SDP: " + sdp.replace(/[\r]/g, '') + "\n");
+          ok(false, "This Transport:IP:Port " + triple + " appears twice in the SDP above!");
         }
-      );
-    }
-  ],
-  [
-    'PC_REMOTE_SANE_LOCAL_SDP',
-    function (test) {
-      test.pcRemote.verifySdp(test._remote_answer, "answer",
-        test._offer_constraints, test._offer_options,
-        function (trickle) {
-          test.pcRemote.localRequiresTrickleIce = trickle;
-        });
-      test.next();
-    }
-  ],
-  [
-    'PC_LOCAL_SANE_REMOTE_SDP',
-    function (test) {
-      test.pcLocal.verifySdp(test._remote_answer, "answer",
-        test._offer_constraints, test._offer_options,
-        function (trickle) {
-          test.pcLocal.remoteRequiresTrickleIce = trickle;
-        });
-      test.next();
-    }
-  ],
-  [
-    'PC_LOCAL_WAIT_FOR_ICE_CONNECTED',
-    function (test) {
-      var myTest = test;
-      var myPc = myTest.pcLocal;
+        resultArray.push(triple);
+      }
+      return resultArray;
+    };
 
-      function onIceConnectedSuccess () {
-        info("pcLocal ICE connection state log: " + test.pcLocal.iceConnectionLog);
-        ok(true, "pc_local: ICE switched to 'connected' state");
-        myTest.next();
-      };
-      function onIceConnectedFailed () {
-        dumpSdp(myTest);
-        ok(false, "pc_local: ICE failed to switch to 'connected' state: " + myPc.iceConnectionState);
-        myTest.next();
-      };
+    var offerTriples = _sdpCandidatesIntoArray(test._local_offer.sdp);
+    info("Offer ICE host candidates: " + JSON.stringify(offerTriples));
 
-      if (myPc.isIceConnected()) {
-        info("pcLocal ICE connection state log: " + test.pcLocal.iceConnectionLog);
-        ok(true, "pc_local: ICE is in connected state");
-        myTest.next();
-      } else if (myPc.isIceConnectionPending()) {
-        myPc.waitForIceConnected(onIceConnectedSuccess, onIceConnectedFailed);
-      } else {
-        dumpSdp(myTest);
-        ok(false, "pc_local: ICE is already in bad state: " + myPc.iceConnectionState);
-        myTest.next();
-      }
-    }
-  ],
-  [
-    'PC_LOCAL_VERIFY_ICE_GATHERING',
-    function (test) {
-      if (test.pcLocal.localRequiresTrickleIce) {
-        ok(test.pcLocal._local_ice_candidates.length > 0, "Received local trickle ICE candidates");
-      }
-      isnot(test.pcLocal._pc.iceGatheringState, GATH_NEW, "ICE gathering state is not 'new'");
-      test.next();
-    }
-  ],
-  [
-    'PC_REMOTE_WAIT_FOR_ICE_CONNECTED',
-    function (test) {
-      var myTest = test;
-      var myPc = myTest.pcRemote;
+    var answerTriples = _sdpCandidatesIntoArray(test.originalAnswer.sdp);
+    info("Answer ICE host candidates: " + JSON.stringify(answerTriples));
 
-      function onIceConnectedSuccess () {
-        info("pcRemote ICE connection state log: " + test.pcRemote.iceConnectionLog);
-        ok(true, "pc_remote: ICE switched to 'connected' state");
-        myTest.next();
-      };
-      function onIceConnectedFailed () {
-        dumpSdp(myTest);
-        ok(false, "pc_remote: ICE failed to switch to 'connected' state: " + myPc.iceConnectionState);
-        myTest.next();
-      };
-
-      if (myPc.isIceConnected()) {
-        info("pcRemote ICE connection state log: " + test.pcRemote.iceConnectionLog);
-        ok(true, "pc_remote: ICE is in connected state");
-        myTest.next();
-      } else if (myPc.isIceConnectionPending()) {
-        myPc.waitForIceConnected(onIceConnectedSuccess, onIceConnectedFailed);
-      } else {
-        dumpSdp(myTest);
-        ok(false, "pc_remote: ICE is already in bad state: " + myPc.iceConnectionState);
-        myTest.next();
-      }
-    }
-  ],
-  [
-    'PC_REMOTE_VERIFY_ICE_GATHERING',
-    function (test) {
-      if (test.pcRemote.localRequiresTrickleIce) {
-        ok(test.pcRemote._local_ice_candidates.length > 0, "Received local trickle ICE candidates");
+    offerTriples.forEach(o => {
+      if (answerTriples.indexOf(o) !== -1) {
+        dump("SDP offer: " + test._local_offer.sdp.replace(/[\r]/g, '') + "\n");
+        dump("SDP answer: " + test.originalAnswer.sdp.replace(/[\r]/g, '') + "\n");
+        ok(false, "This IP:Port " + o + " appears in SDP offer and answer!");
       }
-      isnot(test.pcRemote._pc.iceGatheringState, GATH_NEW, "ICE gathering state is not 'new'");
-      test.next();
-    }
-  ],
-  [
-    'PC_LOCAL_CHECK_MEDIA_TRACKS',
-    function (test) {
-      test.pcLocal.checkMediaTracks(test._answer_constraints, function () {
-        test.next();
-      });
-    }
-  ],
-  [
-    'PC_REMOTE_CHECK_MEDIA_TRACKS',
-    function (test) {
-      test.pcRemote.checkMediaTracks(test._offer_constraints, function () {
-        test.next();
+    });
+  },
+
+  function PC_REMOTE_SET_LOCAL_DESCRIPTION(test) {
+    return test.setLocalDescription(test.pcRemote, test.originalAnswer, STABLE)
+      .then(() => {
+        is(test.pcRemote.signalingState, STABLE,
+           "signalingState after remote setLocalDescription is 'stable'");
       });
-    }
-  ],
-  [
-    'PC_LOCAL_CHECK_MEDIA_FLOW_PRESENT',
-    function (test) {
-      test.pcLocal.checkMediaFlowPresent(function () {
-        test.next();
-      });
-    }
-  ],
-  [
-    'PC_REMOTE_CHECK_MEDIA_FLOW_PRESENT',
-    function (test) {
-      test.pcRemote.checkMediaFlowPresent(function () {
-        test.next();
-      });
-    }
-  ],
-  [
-    'PC_LOCAL_CHECK_MSID',
-    function (test) {
-      test.pcLocal.checkMsids();
-      test.next();
+  },
+
+  function PC_LOCAL_GET_ANSWER(test) {
+    if (!test.steeplechase) {
+      test._remote_answer = test.originalAnswer;
+      test._answer_constraints = test.pcRemote.constraints;
+      return Promise.resolve();
     }
-  ],
-  [
-    'PC_REMOTE_CHECK_MSID',
-    function (test) {
-      test.pcRemote.checkMsids();
-      test.next();
-    }
-  ],
-  [
-    'PC_LOCAL_CHECK_STATS',
-    function (test) {
-      test.pcLocal.getStats(null, function(stats) {
-        test.pcLocal.checkStats(stats, test.steeplechase);
-        test.next();
-      });
-    }
-  ],
-  [
-    'PC_REMOTE_CHECK_STATS',
-    function (test) {
-      test.pcRemote.getStats(null, function(stats) {
-        test.pcRemote.checkStats(stats, test.steeplechase);
-        test.next();
-      });
-    }
-  ],
-  [
-    'PC_LOCAL_CHECK_ICE_CONNECTION_TYPE',
-    function (test) {
-      test.pcLocal.getStats(null, function(stats) {
-        test.pcLocal.checkStatsIceConnectionType(stats);
-        test.next();
-      });
-    }
-  ],
-  [
-    'PC_REMOTE_CHECK_ICE_CONNECTION_TYPE',
-    function (test) {
-      test.pcRemote.getStats(null, function(stats) {
-        test.pcRemote.checkStatsIceConnectionType(stats);
-        test.next();
-      });
-    }
-  ],
-  [
-    'PC_LOCAL_CHECK_ICE_CONNECTIONS',
-    function (test) {
-      test.pcLocal.getStats(null, function(stats) {
-        test.pcLocal.checkStatsIceConnections(stats,
-                                              test._offer_constraints,
-                                              test._offer_options,
-                                              test._remote_answer);
-        test.next();
-      });
-    }
-  ],
-  [
-    'PC_REMOTE_CHECK_ICE_CONNECTIONS',
-    function (test) {
-      test.pcRemote.getStats(null, function(stats) {
-        test.pcRemote.checkStatsIceConnections(stats,
-                                               test._offer_constraints,
-                                               test._offer_options,
-                                               test.originalAnswer);
-        test.next();
+
+    return test.getSignalingMessage("answer").then(message => {
+      ok("answer" in message, "Got an answer message");
+      test._remote_answer = new mozRTCSessionDescription(message.answer);
+      test._answer_constraints = message.answer_constraints;
+    });
+  },
+
+  function PC_LOCAL_SET_REMOTE_DESCRIPTION(test) {
+    test.setRemoteDescription(test.pcLocal, test._remote_answer, STABLE)
+      .then(() => {
+        is(test.pcLocal.signalingState, STABLE,
+           "signalingState after local setRemoteDescription is 'stable'");
       });
-    }
-  ],
-  [
-    'PC_LOCAL_CHECK_GETSTATS_AUDIOTRACK_OUTBOUND',
-    function (test) {
-      var pc = test.pcLocal;
-      var stream = pc._pc.getLocalStreams()[0];
-      var track = stream && stream.getAudioTracks()[0];
-      if (track) {
-        var msg = "pcLocal.HasStat outbound audio rtp ";
-        pc.getStats(track, function(stats) {
-          ok(pc.hasStat(stats,
-                        { type:"outboundrtp", isRemote:false, mediaType:"audio" }),
-             msg + "1");
-          ok(!pc.hasStat(stats, { type:"inboundrtp", isRemote:false }), msg + "2");
-          ok(!pc.hasStat(stats, { mediaType:"video" }), msg + "3");
-          test.next();
-        });
-      } else {
-        test.next();
-      }
-    }
-  ],
-  [
-    'PC_LOCAL_CHECK_GETSTATS_VIDEOTRACK_OUTBOUND',
-    function (test) {
-      var pc = test.pcLocal;
-      var stream = pc._pc.getLocalStreams()[0];
-      var track = stream && stream.getVideoTracks()[0];
-      if (track) {
-        var msg = "pcLocal.HasStat outbound video rtp ";
-        pc.getStats(track, function(stats) {
-          ok(pc.hasStat(stats,
-                        { type:"outboundrtp", isRemote:false, mediaType:"video" }),
-             msg + "1");
-          ok(!pc.hasStat(stats, { type:"inboundrtp", isRemote:false }), msg + "2");
-          ok(!pc.hasStat(stats, { mediaType:"audio" }), msg + "3");
-          test.next();
-        });
-      } else {
-        test.next();
-      }
-    }
-  ],
-  [
-    'PC_LOCAL_CHECK_GETSTATS_AUDIOTRACK_INBOUND',
-    function (test) {
-      var pc = test.pcLocal;
-      var stream = pc._pc.getRemoteStreams()[0];
-      var track = stream && stream.getAudioTracks()[0];
-      if (track) {
-        var msg = "pcLocal.HasStat inbound audio rtp ";
-        pc.getStats(track, function(stats) {
-          ok(pc.hasStat(stats,
-                        { type:"inboundrtp", isRemote:false, mediaType:"audio" }),
-             msg + "1");
-          ok(!pc.hasStat(stats, { type:"outboundrtp", isRemote:false }), msg + "2");
-          ok(!pc.hasStat(stats, { mediaType:"video" }), msg + "3");
-          test.next();
-        });
-      } else {
-        test.next();
-      }
-    }
-  ],
-  [
-    'PC_LOCAL_CHECK_GETSTATS_VIDEOTRACK_INBOUND',
-    function (test) {
-      var pc = test.pcLocal;
-      var stream = pc._pc.getRemoteStreams()[0];
-      var track = stream && stream.getVideoTracks()[0];
-      if (track) {
-        var msg = "pcLocal.HasStat inbound video rtp ";
-        pc.getStats(track, function(stats) {
-          ok(pc.hasStat(stats,
-                        { type:"inboundrtp", isRemote:false, mediaType:"video" }),
-             msg + "1");
-          ok(!pc.hasStat(stats, { type:"outboundrtp", isRemote:false }), msg + "2");
-          ok(!pc.hasStat(stats, { mediaType:"audio" }), msg + "3");
-          test.next();
-        });
-      } else {
-        test.next();
-      }
-    }
-  ],
-  [
-    'PC_REMOTE_CHECK_GETSTATS_AUDIOTRACK_OUTBOUND',
-    function (test) {
-      var pc = test.pcRemote;
-      var stream = pc._pc.getLocalStreams()[0];
-      var track = stream && stream.getAudioTracks()[0];
-      if (track) {
-        var msg = "pcRemote.HasStat outbound audio rtp ";
-        pc.getStats(track, function(stats) {
-          ok(pc.hasStat(stats,
-                        { type:"outboundrtp", isRemote:false, mediaType:"audio" }),
-             msg + "1");
-          ok(!pc.hasStat(stats, { type:"inboundrtp", isRemote:false }), msg + "2");
-          ok(!pc.hasStat(stats, { mediaType:"video" }), msg + "3");
-          test.next();
-        });
-      } else {
-        test.next();
-      }
-    }
-  ],
-  [
-    'PC_REMOTE_CHECK_GETSTATS_VIDEOTRACK_OUTBOUND',
-    function (test) {
-      var pc = test.pcRemote;
-      var stream = pc._pc.getLocalStreams()[0];
-      var track = stream && stream.getVideoTracks()[0];
-      if (track) {
-        var msg = "pcRemote.HasStat outbound audio rtp ";
-        pc.getStats(track, function(stats) {
-          ok(pc.hasStat(stats,
-                        { type:"outboundrtp", isRemote:false, mediaType:"video" }),
-             msg + "1");
-          ok(!pc.hasStat(stats, { type:"inboundrtp", isRemote:false }), msg + "2");
-          ok(!pc.hasStat(stats, { mediaType:"audio" }), msg + "3");
-          test.next();
-        });
-      } else {
-        test.next();
-      }
-    }
-  ],
-  [
-    'PC_REMOTE_CHECK_GETSTATS_AUDIOTRACK_INBOUND',
-    function (test) {
-      var pc = test.pcRemote;
-      var stream = pc._pc.getRemoteStreams()[0];
-      var track = stream && stream.getAudioTracks()[0];
-      if (track) {
-        var msg = "pcRemote.HasStat inbound audio rtp ";
-        pc.getStats(track, function(stats) {
-          ok(pc.hasStat(stats,
-                        { type:"inboundrtp", isRemote:false, mediaType:"audio" }),
-             msg + "1");
-          ok(!pc.hasStat(stats, { type:"outboundrtp", isRemote:false }), msg + "2");
-          ok(!pc.hasStat(stats, { mediaType:"video" }), msg + "3");
-          test.next();
-        });
-      } else {
-        test.next();
-      }
-    }
-  ],
-  [
-    'PC_REMOTE_CHECK_GETSTATS_VIDEOTRACK_INBOUND',
-    function (test) {
-      var pc = test.pcRemote;
-      var stream = pc._pc.getRemoteStreams()[0];
-      var track = stream && stream.getVideoTracks()[0];
-      if (track) {
-        var msg = "pcRemote.HasStat inbound video rtp ";
-        pc.getStats(track, function(stats) {
-          ok(pc.hasStat(stats,
-                        { type:"inboundrtp", isRemote:false, mediaType:"video" }),
-             msg + "1");
-          ok(!pc.hasStat(stats, { type:"outboundrtp", isRemote:false }), msg + "2");
-          ok(!pc.hasStat(stats, { mediaType:"audio" }), msg + "3");
-          test.next();
-        });
-      } else {
-        test.next();
-      }
-    }
-  ]
+  },
+  function PC_REMOTE_SANE_LOCAL_SDP(test) {
+    test.pcRemote.verifySdp(test._remote_answer, "answer",
+                            test._offer_constraints, test._offer_options,
+                            true);
+  },
+  function PC_LOCAL_SANE_REMOTE_SDP(test) {
+    test.pcLocal.verifySdp(test._remote_answer, "answer",
+                           test._offer_constraints, test._offer_options,
+                           false);
+  },
+
+  function PC_LOCAL_WAIT_FOR_ICE_CONNECTED(test) {
+    return waitForIceConnected(test, test.pcLocal);
+  },
+
+  function PC_REMOTE_WAIT_FOR_ICE_CONNECTED(test) {
+    return waitForIceConnected(test, test.pcRemote);
+  },
+
+  function PC_LOCAL_VERIFY_ICE_GATHERING(test) {
+    return waitForAnIceCandidate(test.pcLocal);
+  },
+
+  function PC_REMOTE_VERIFY_ICE_GATHERING(test) {
+    return waitForAnIceCandidate(test.pcRemote);
+  },
+
+  function PC_LOCAL_CHECK_MEDIA_TRACKS(test) {
+    return test.pcLocal.checkMediaTracks(test._answer_constraints);
+  },
+
+  function PC_REMOTE_CHECK_MEDIA_TRACKS(test) {
+    return test.pcRemote.checkMediaTracks(test._offer_constraints);
+  },
+
+  function PC_LOCAL_CHECK_MEDIA_FLOW_PRESENT(test) {
+    return test.pcLocal.checkMediaFlowPresent();
+  },
+
+  function PC_REMOTE_CHECK_MEDIA_FLOW_PRESENT(test) {
+    return test.pcRemote.checkMediaFlowPresent();
+  },
+/* TODO: re-enable when Bug 1095218 lands
+  function PC_LOCAL_CHECK_MSID(test) {
+    test.pcLocal.checkMsids();
+  },
+  function PC_REMOTE_CHECK_MSID(test) {
+    test.pcRemote.checkMsids();
+  },
+*/
+  function PC_LOCAL_CHECK_STATS(test) {
+    return test.pcLocal.getStats(null).then(stats => {
+      test.pcLocal.checkStats(stats, test.steeplechase);
+    });
+  },
+
+  function PC_REMOTE_CHECK_STATS(test) {
+    test.pcRemote.getStats(null).then(stats => {
+      test.pcRemote.checkStats(stats, test.steeplechase);
+    });
+  },
+
+  function PC_LOCAL_CHECK_ICE_CONNECTION_TYPE(test) {
+    test.pcLocal.getStats(null).then(stats => {
+      test.pcLocal.checkStatsIceConnectionType(stats);
+    });
+  },
+
+  function PC_REMOTE_CHECK_ICE_CONNECTION_TYPE(test) {
+    test.pcRemote.getStats(null).then(stats => {
+      test.pcRemote.checkStatsIceConnectionType(stats);
+    });
+  },
+
+  function PC_LOCAL_CHECK_ICE_CONNECTIONS(test) {
+    test.pcLocal.getStats(null).then(stats => {
+      test.pcLocal.checkStatsIceConnections(stats,
+                                            test._offer_constraints,
+                                            test._offer_options,
+                                            test._remote_answer);
+    });
+  },
+
+  function PC_REMOTE_CHECK_ICE_CONNECTIONS(test) {
+    test.pcRemote.getStats(null).then(stats => {
+      test.pcRemote.checkStatsIceConnections(stats,
+                                             test._offer_constraints,
+                                             test._offer_options,
+                                             test.originalAnswer);
+    });
+  },
+
+  function PC_LOCAL_CHECK_MSID(test) {
+    return test.pcLocal.checkMsids();
+  },
+  function PC_REMOTE_CHECK_MSID(test) {
+    return test.pcRemote.checkMsids();
+  },
+
+  function PC_LOCAL_CHECK_STATS(test) {
+    return checkAllTrackStats(test.pcLocal);
+  },
+  function PC_REMOTE_CHECK_STATS(test) {
+    return checkAllTrackStats(test.pcRemote);
+  }
 ];
-
--- a/dom/media/tests/mochitest/test_dataChannel_basicAudio.html
+++ b/dom/media/tests/mochitest/test_dataChannel_basicAudio.html
@@ -1,18 +1,12 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="dataChannel.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "796895",
     title: "Basic data channel audio connection"
   });
--- a/dom/media/tests/mochitest/test_dataChannel_basicAudioVideo.html
+++ b/dom/media/tests/mochitest/test_dataChannel_basicAudioVideo.html
@@ -1,18 +1,12 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="dataChannel.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "796891",
     title: "Basic data channel audio/video connection"
   });
--- a/dom/media/tests/mochitest/test_dataChannel_basicAudioVideoCombined.html
+++ b/dom/media/tests/mochitest/test_dataChannel_basicAudioVideoCombined.html
@@ -1,18 +1,12 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="dataChannel.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "796891",
     title: "Basic data channel audio/video connection"
   });
--- a/dom/media/tests/mochitest/test_dataChannel_basicAudioVideoNoBundle.html
+++ b/dom/media/tests/mochitest/test_dataChannel_basicAudioVideoNoBundle.html
@@ -1,44 +1,33 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="dataChannel.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "1016476",
     title: "Basic data channel audio/video connection without bundle"
   });
 
-  var test;
-  runNetworkTest(function () {
-    test = new PeerConnectionTest();
-    addInitialDataChannel(test.chain);
-    test.chain.insertAfter("PC_LOCAL_CREATE_OFFER",
-      [[
-        'PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER',
-        function (test) {
-          // Just replace a=group:BUNDLE with something that will be ignored.
-          test.originalOffer.sdp = test.originalOffer.sdp.replace(
-            "a=group:BUNDLE",
-            "a=foo:");
-          test.next();
-        }
-      ]]
-      );
-    test.setMediaConstraints([{audio: true}, {video: true}],
-                             [{audio: true}, {video: true}]);
-    test.run();
-  });
-
+var test;
+runNetworkTest(function () {
+  test = new PeerConnectionTest();
+  addInitialDataChannel(test.chain);
+  test.chain.insertAfter("PC_LOCAL_CREATE_OFFER", [
+    function PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER(test) {
+      // Just replace a=group:BUNDLE with something that will be ignored.
+      test.originalOffer.sdp = test.originalOffer.sdp.replace(
+        "a=group:BUNDLE",
+        "a=foo:");
+    }
+  ]);
+  test.setMediaConstraints([{audio: true}, {video: true}],
+                           [{audio: true}, {video: true}]);
+  test.run();
+});
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_dataChannel_basicDataOnly.html
+++ b/dom/media/tests/mochitest/test_dataChannel_basicDataOnly.html
@@ -1,18 +1,12 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="dataChannel.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "796894",
     title: "Basic datachannel only connection"
   });
--- a/dom/media/tests/mochitest/test_dataChannel_basicVideo.html
+++ b/dom/media/tests/mochitest/test_dataChannel_basicVideo.html
@@ -1,18 +1,12 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="dataChannel.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "796889",
     title: "Basic data channel video connection"
   });
--- a/dom/media/tests/mochitest/test_dataChannel_bug1013809.html
+++ b/dom/media/tests/mochitest/test_dataChannel_bug1013809.html
@@ -1,18 +1,12 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="dataChannel.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "796895",
     title: "Basic data channel audio connection"
   });
--- a/dom/media/tests/mochitest/test_dataChannel_noOffer.html
+++ b/dom/media/tests/mochitest/test_dataChannel_noOffer.html
@@ -1,14 +1,11 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="pc.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "856319",
     title: "Don't offer m=application unless createDataChannel is called first"
--- a/dom/media/tests/mochitest/test_getUserMedia_basicAudio.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_basicAudio.html
@@ -1,46 +1,29 @@
 <!DOCTYPE HTML>
 <html>
-<!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=781534
--->
 <head>
-  <meta charset="utf-8">
-  <title>mozGetUserMedia Basic Audio Test</title>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="mediaStreamPlayback.js"></script>
 </head>
 <body>
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=781534">mozGetUserMedia Basic Audio Test</a>
-<p id="display"></p>
-<div id="content" style="display: none">
-  <audio id="testAudio"></audio>
-</div>
 <pre id="test">
 <script type="application/javascript">
+  createHTML({ title: "getUserMedia Basic Audio Test", bug: "781534" });
   /**
    * Run a test to verify that we can complete a start and stop media playback
    * cycle for an audio LocalMediaStream on an audio HTMLMediaElement.
    */
   runTest(function () {
-    var testAudio = document.getElementById('testAudio');
+    var testAudio = createMediaElement('audio', 'testAudio');
     var constraints = {audio: true};
 
-    getUserMedia(constraints, function (aStream) {
+    getUserMedia(constraints).then(aStream => {
       checkMediaStreamTracks(constraints, aStream);
 
       var playback = new LocalMediaStreamPlayback(testAudio, aStream);
-      playback.playMedia(false, function () {
-        aStream.stop();
-        SimpleTest.finish();
-      }, generateErrorCallback());
-
-    }, generateErrorCallback());
-
+      return playback.playMedia(false);
+    }).then(() => SimpleTest.finish(), generateErrorCallback());
   });
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_getUserMedia_basicScreenshare.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_basicScreenshare.html
@@ -1,58 +1,45 @@
 <!DOCTYPE HTML>
 <html>
-<!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=983504
--->
 <head>
-  <meta charset="utf-8">
-  <title>mozGetUserMedia Basic Screenshare Test</title>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="mediaStreamPlayback.js"></script>
 </head>
 <body>
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=983504">mozGetUserMedia Basic Screenshare Test</a>
-<p id="display"></p>
-<div id="content" style="display: none">
-  <video id="testVideo"></video>
-</div>
 <pre id="test">
 <script type="application/javascript">
+  createHTML({
+    title: "getUserMedia Basic Screenshare Test",
+    bug: "983504"
+  });
   /**
    * Run a test to verify that we can complete a start and stop media playback
    * cycle for an screenshare LocalMediaStream on a video HTMLMediaElement.
    */
   runTest(function () {
     const isWinXP = navigator.userAgent.indexOf("Windows NT 5.1") != -1;
     if (IsMacOSX10_6orOlder() || isWinXP) {
         ok(true, "Screensharing disabled for OSX10.6 and WinXP");
         SimpleTest.finish();
         return;
     }
-    var testVideo = document.getElementById('testVideo');
+    var testVideo = createMediaElement('video', 'testVideo');
     var constraints = {
       video: {
          mozMediaSource: "screen",
          mediaSource: "screen"
       },
       fake: false
     };
 
-    getUserMedia(constraints, function (aStream) {
+    getUserMedia(constraints).then(aStream => {
       checkMediaStreamTracks(constraints, aStream);
 
       var playback = new LocalMediaStreamPlayback(testVideo, aStream);
-      playback.playMediaWithStreamStop(false, function () {
-        aStream.stop();
-        SimpleTest.finish();
-      }, generateErrorCallback());
-
-    }, generateErrorCallback());
+      return playback.playMediaWithStreamStop(false);
+    }).then(() => SimpleTest.finish(), generateErrorCallback());
 
   });
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_getUserMedia_basicVideo.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_basicVideo.html
@@ -1,46 +1,32 @@
 <!DOCTYPE HTML>
 <html>
-<!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=781534
--->
 <head>
-  <meta charset="utf-8">
-  <title>mozGetUserMedia Basic Video Test</title>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="mediaStreamPlayback.js"></script>
 </head>
 <body>
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=781534">mozGetUserMedia Basic Video Test</a>
-<p id="display"></p>
-<div id="content" style="display: none">
-  <video id="testVideo"></video>
-</div>
 <pre id="test">
 <script type="application/javascript">
+  createHTML({
+    title: "getUserMedia Basic Video Test",
+    bug: "781534"
+  });
   /**
    * Run a test to verify that we can complete a start and stop media playback
    * cycle for an video LocalMediaStream on a video HTMLMediaElement.
    */
   runTest(function () {
-    var testVideo = document.getElementById('testVideo');
+    var testVideo = createMediaElement('video', 'testVideo');
     var constraints = {video: true};
 
-    getUserMedia(constraints, function (aStream) {
+    getUserMedia(constraints).then(aStream => {
       checkMediaStreamTracks(constraints, aStream);
 
       var playback = new LocalMediaStreamPlayback(testVideo, aStream);
-      playback.playMedia(false, function () {
-        aStream.stop();
-        SimpleTest.finish();
-      }, generateErrorCallback());
-
-    }, generateErrorCallback());
-
+      return playback.playMedia(false);
+    }).then(() => SimpleTest.finish(), generateErrorCallback());
   });
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_getUserMedia_basicVideoAudio.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_basicVideoAudio.html
@@ -1,45 +1,32 @@
 <!DOCTYPE HTML>
 <html>
-<!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=781534
--->
 <head>
-  <meta charset="utf-8">
-  <title>mozGetUserMedia Basic Video & Audio Test</title>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="mediaStreamPlayback.js"></script>
 </head>
 <body>
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=781534">mozGetUserMedia Basic Video & Audio Test</a>
-<p id="display"></p>
-<div id="content" style="display: none">
-  <video id="testVideoAudio"></video>
-</div>
 <pre id="test">
 <script type="application/javascript">
+  createHTML({
+    title: "getUserMedia Basic Video & Audio Test",
+    bug: "781534"
+  });
   /**
    * Run a test to verify that we can complete a start and stop media playback
    * cycle for a video and audio LocalMediaStream on a video HTMLMediaElement.
    */
   runTest(function () {
-    var testVideoAudio = document.getElementById('testVideoAudio');
+    var testVideoAudio = createMediaElement('video', 'testVideoAudio');
     var constraints = {video: true, audio: true};
 
-    getUserMedia(constraints, function (aStream) {
+    getUserMedia(constraints).then(aStream => {
       checkMediaStreamTracks(constraints, aStream);
 
       var playback = new LocalMediaStreamPlayback(testVideoAudio, aStream);
-      playback.playMedia(false, function () {
-        aStream.stop();
-        SimpleTest.finish();
-      }, generateErrorCallback());
-
-    }, generateErrorCallback());
+      return playback.playMedia(false);
+    }).then(() => SimpleTest.finish(), generateErrorCallback());
   });
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_getUserMedia_basicWindowshare.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_basicWindowshare.html
@@ -1,58 +1,45 @@
 <!DOCTYPE HTML>
 <html>
-<!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=983504
--->
 <head>
-  <meta charset="utf-8">
-  <title>mozGetUserMedia Basic Windowshare Test</title>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="mediaStreamPlayback.js"></script>
 </head>
 <body>
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1038926">mozGetUserMedia Basic Windowshare Test</a>
-<p id="display"></p>
-<div id="content" style="display: none">
-  <video id="testVideo"></video>
-</div>
 <pre id="test">
 <script type="application/javascript">
+  createHTML({
+    title: "getUserMedia Basic Windowshare Test",
+    bug: "1038926"
+  });
   /**
    * Run a test to verify that we can complete a start and stop media playback
    * cycle for an screenshare LocalMediaStream on a video HTMLMediaElement.
    */
   runTest(function () {
     const isWinXP = navigator.userAgent.indexOf("Windows NT 5.1") != -1;
     if (IsMacOSX10_6orOlder() || isWinXP) {
         ok(true, "Screensharing disabled for OSX10.6 and WinXP");
         SimpleTest.finish();
         return;
     }
-    var testVideo = document.getElementById('testVideo');
+    var testVideo = createMediaElement('video', 'testVideo');
     var constraints = {
       video: {
          mozMediaSource: "window",
          mediaSource: "window"
       },
       fake: false
     };
 
-    getUserMedia(constraints, function (aStream) {
+    getUserMedia(constraints).then(aStream => {
       checkMediaStreamTracks(constraints, aStream);
 
       var playback = new LocalMediaStreamPlayback(testVideo, aStream);
-      playback.playMediaWithStreamStop(false, function () {
-        aStream.stop();
-        SimpleTest.finish();
-      }, generateErrorCallback());
-
-    }, generateErrorCallback());
+      return playback.playMediaWithStreamStop(false);
+    }).then(() => SimpleTest.finish(), generateErrorCallback());
 
   });
 
 </script>
 </pre>
 </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_callbacks.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({
+    title: "navigator.mozGetUserMedia Callback Test",
+    bug: "1119593"
+  });
+  /**
+   * Check that the old fashioned callback-based function works.
+   */
+  runTest(function () {
+    var testAudio = createMediaElement('audio', 'testAudio');
+    var constraints = {audio: true};
+
+    SimpleTest.waitForExplicitFinish();
+    navigator.mozGetUserMedia(constraints, aStream => {
+      checkMediaStreamTracks(constraints, aStream);
+
+      var playback = new LocalMediaStreamPlayback(testAudio, aStream);
+      return playback.playMedia(false)
+        .then(() => SimpleTest.finish(), generateErrorCallback());
+    }, generateErrorCallback());
+  });
+
+</script>
+</pre>
+</body>
+</html>
--- a/dom/media/tests/mochitest/test_getUserMedia_constraints.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_constraints.html
@@ -1,29 +1,18 @@
 <!DOCTYPE HTML>
 <html>
-<!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=882145
--->
 <head>
-  <meta charset="utf-8">
-  <title>Test mozGetUserMedia Constraints</title>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
-  <script type="application/javascript" src="constraints.js"></script>
+  <script src="mediaStreamPlayback.js"></script>
+  <script src="constraints.js"></script>
 </head>
 <body>
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=882145">Test mozGetUserMedia Constraints (desktop)</a>
-<p id="display"></p>
-<div id="content" style="display: none">
-
-</div>
 <pre id="test">
 <script type="application/javascript">
+createHTML({ title: "Test getUserMedia constraints (desktop)", bug: "882145" });
 /**
   See constraints.js for testConstraints() and common_tests.
   TODO(jib): Merge desktop and mobile version of these tests again (Bug 997365)
 */
 var desktop_tests = [
   { message: "legacy facingMode ignored (desktop)",
     constraints: { video: { mandatory: { facingMode:'left' } }, fake: true },
     error: null },
--- a/dom/media/tests/mochitest/test_getUserMedia_constraints_mobile.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_constraints_mobile.html
@@ -1,29 +1,18 @@
 <!DOCTYPE HTML>
 <html>
-<!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=882145
--->
 <head>
-  <meta charset="utf-8">
-  <title>Test mozGetUserMedia Constraints</title>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
-  <script type="application/javascript" src="constraints.js"></script>
+  <script src="mediaStreamPlayback.js"></script>
+  <script src="constraints.js"></script>
 </head>
 <body>
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=882145">Test mozGetUserMedia Constraints (mobile)</a>
-<p id="display"></p>
-<div id="content" style="display: none">
-
-</div>
 <pre id="test">
 <script type="application/javascript">
+createHTML({ title: "Test getUserMedia constraints (mobile)", bug: "882145" });
 /**
   See constraints.js for testConstraints() and common_tests.
   TODO(jib): Merge desktop and mobile version of these tests again (Bug 997365)
 */
 var mobile_tests = [
   { message: "legacy facingMode overconstrains video (mobile)",
     constraints: { video: { mandatory: { facingMode:'left' } }, fake: true },
     error: "NotFoundError" },
--- a/dom/media/tests/mochitest/test_getUserMedia_gumWithinGum.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_gumWithinGum.html
@@ -1,56 +1,40 @@
-<!DOCTYPE HTML>
+<!DOCTYPE HTML>
 <html>
-<!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=822109
--->
 <head>
-  <meta charset="utf-8">
-  <title>mozGetUserMedia gum within gum</title>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="mediaStreamPlayback.js"></script>
 </head>
 <body>
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=822109">mozGetUserMedia gum within gum</a>
-<p id="display"></p>
-<div id="content" style="display: none">
-  <video id="testVideo"></video>
-  <audio id="testAudio"></audio>
-</div>
 <pre id="test">
 <script type="application/javascript">
+  createHTML({title: "getUserMedia within getUserMedia", bug: "822109" });
   /**
    * Run a test that we can complete a playback cycle for a video,
    * then upon completion, do a playback cycle with audio, such that
    * the audio gum call happens within the video gum call.
    */
   runTest(function () {
-    getUserMedia({video: true}, function(videoStream) {
-      var testVideo = document.getElementById('testVideo');
-      var videoStreamPlayback = new LocalMediaStreamPlayback(testVideo,
-        videoStream);
-
-      videoStreamPlayback.playMedia(false, function() {
-        getUserMedia({audio: true}, function(audioStream) {
-          var testAudio = document.getElementById('testAudio');
-          var audioStreamPlayback = new LocalMediaStreamPlayback(testAudio,
-            audioStream);
+    getUserMedia({video: true})
+      .then(videoStream => {
+        var testVideo = createMediaElement('video', 'testVideo');
+        var videoPlayback = new LocalMediaStreamPlayback(testVideo,
+                                                         videoStream);
 
-          audioStreamPlayback.playMedia(false, function() {
-            audioStream.stop();
-            videoStream.stop();
-            SimpleTest.finish();
-          }, generateErrorCallback());
+        return videoPlayback.playMedia(false)
+          .then(() => getUserMedia({audio: true}))
+          .then(audioStream => {
+            var testAudio = createMediaElement('audio', 'testAudio');
+            var audioPlayback = new LocalMediaStreamPlayback(testAudio,
+                                                             audioStream);
 
-        }, generateErrorCallback());
-
-      }, generateErrorCallback());
-
-    }, generateErrorCallback());
+            return audioPlayback.playMedia(false)
+              .then(() => audioStream.stop());
+          })
+          .then(() => videoStream.stop());
+      })
+      .then(() => SimpleTest.finish(), generateErrorCallback());
   });
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_getUserMedia_peerIdentity.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_peerIdentity.html
@@ -1,29 +1,18 @@
 <!DOCTYPE HTML>
 <html>
-<!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=942367
--->
 <head>
-  <meta charset="utf-8">
-  <title>Test mozGetUserMedia peerIdentity Constraint</title>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
+  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
   <script type="application/javascript" src="blacksilence.js"></script>
 </head>
 <body>
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=942367">Test mozGetUserMedia peerIdentity Constraint</a>
-<p id="display"></p>
-<div id="content" style="display: none">
-
-</div>
 <pre id="test">
 <script type="application/javascript">
+createHTML({ title: "Test getUserMedia peerIdentity Constraint", bug: "942367" });
 function theTest() {
   function testPeerIdentityConstraint(withConstraint, done) {
     var config = { audio: true, video: true, fake: true };
     if (withConstraint) {
       config.peerIdentity = 'user@example.com';
     }
     info('getting media with constraints: ' + JSON.stringify(config));
     navigator.mediaDevices.getUserMedia(config).then(function(stream) {
--- a/dom/media/tests/mochitest/test_getUserMedia_playAudioTwice.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_playAudioTwice.html
@@ -1,46 +1,26 @@
-<!DOCTYPE HTML>
+<!DOCTYPE HTML>
 <html>
-<!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=822109
--->
 <head>
-  <meta charset="utf-8">
-  <title>mozGetUserMedia Play Audio Twice</title>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="mediaStreamPlayback.js"></script>
 </head>
 <body>
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=822109">mozGetUserMedia Play Audio Twice</a>
-<p id="display"></p>
-<div id="content" style="display: none">
-  <audio id="testAudio"></audio>
-</div>
 <pre id="test">
 <script type="application/javascript">
+  createHTML({title: "getUserMedia Play Audio Twice", bug: "822109" });
   /**
    * Run a test that we can complete an audio playback cycle twice in a row.
    */
   runTest(function () {
-    getUserMedia({audio: true}, function(audioStream) {
-      var testAudio = document.getElementById('testAudio');
-      var audioStreamPlayback = new LocalMediaStreamPlayback(testAudio,
-        audioStream);
-
-      audioStreamPlayback.playMedia(false, function() {
+    getUserMedia({audio: true}).then(audioStream => {
+      var testAudio = createMediaElement('audio', 'testAudio');
+      var playback = new LocalMediaStreamPlayback(testAudio, audioStream);
 
-        audioStreamPlayback.playMedia(true, function() {
-          audioStream.stop();
-          SimpleTest.finish();
-        }, generateErrorCallback());
-
-      }, generateErrorCallback());
-
-    }, generateErrorCallback());
+      return playback.playMedia(false)
+        .then(() => playback.playMedia(true))
+        .then(() => audioStream.stop());
+    }).then(() => SimpleTest.finish(), generateErrorCallback());
   });
-
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_getUserMedia_playVideoAudioTwice.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_playVideoAudioTwice.html
@@ -1,45 +1,27 @@
-<!DOCTYPE HTML>
+<!DOCTYPE HTML>
 <html>
-<!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=822109
--->
 <head>
-  <meta charset="utf-8">
-  <title>mozGetUserMedia Play Video and Audio Twice</title>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="mediaStreamPlayback.js"></script>
 </head>
 <body>
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=822109">mozGetUserMedia Play Video and Audio Twice</a>
-<p id="display"></p>
-<div id="content" style="display: none">
-  <video id="testVideo"></video>
-</div>
 <pre id="test">
 <script type="application/javascript">
+  createHTML({title: "getUserMedia Play Video and Audio Twice", bug: "822109" });
   /**
    * Run a test that we can complete a video playback cycle twice in a row.
    */
   runTest(function () {
-    getUserMedia({video: true, audio: true}, function(stream) {
-      var testVideo = document.getElementById('testVideo');
-      var streamPlayback = new LocalMediaStreamPlayback(testVideo, stream);
-
-      streamPlayback.playMedia(false, function() {
+    getUserMedia({video: true, audio: true}).then(stream => {
+      var testVideo = createMediaElement('video', 'testVideo');
+      var playback = new LocalMediaStreamPlayback(testVideo, stream);
 
-        streamPlayback.playMedia(true, function() {
-          stream.stop();
-          SimpleTest.finish();
-        }, generateErrorCallback());
-
-      }, generateErrorCallback());
-
-    }, generateErrorCallback());
+      return playback.playMedia(false)
+        .then(() => playback.playMedia(true))
+        .then(() => stream.stop());
+    }).then(() => SimpleTest.finish(), generateErrorCallback());
   });
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_getUserMedia_playVideoTwice.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_playVideoTwice.html
@@ -1,46 +1,27 @@
-<!DOCTYPE HTML>
+<!DOCTYPE HTML>
 <html>
-<!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=822109
--->
 <head>
-  <meta charset="utf-8">
-  <title>mozGetUserMedia Play Video Twice</title>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="mediaStreamPlayback.js"></script>
 </head>
 <body>
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=822109">mozGetUserMedia Play Video Twice</a>
-<p id="display"></p>
-<div id="content" style="display: none">
-  <video id="testVideo"></video>
-</div>
 <pre id="test">
 <script type="application/javascript">
+  createHTML({ title: "getUserMedia Play Video Twice", bug: "822109" });
   /**
    * Run a test that we can complete a video playback cycle twice in a row.
    */
   runTest(function () {
-    getUserMedia({video: true}, function(videoStream) {
-      var testVideo = document.getElementById('testVideo');
-      var videoStreamPlayback = new LocalMediaStreamPlayback(testVideo,
-        videoStream);
-
-      videoStreamPlayback.playMedia(false, function() {
+    getUserMedia({video: true}).then(stream => {
+      var testVideo = createMediaElement('video', 'testVideo');
+      var streamPlayback = new LocalMediaStreamPlayback(testVideo, stream);
 
-        videoStreamPlayback.playMedia(true, function() {
-          videoStream.stop();
-          SimpleTest.finish();
-        }, generateErrorCallback());
-
-      }, generateErrorCallback());
-
-    }, generateErrorCallback());
+      return streamPlayback.playMedia(false)
+        .then(() => streamPlayback.playMedia(true))
+        .then(() => stream.stop());
+    }).then(() => SimpleTest.finish(), generateErrorCallback());
   });
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_getUserMedia_stopAudioStream.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_stopAudioStream.html
@@ -1,39 +1,28 @@
-<!DOCTYPE HTML>
+<!DOCTYPE HTML>
 <html>
-<!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=822109
--->
 <head>
-  <meta charset="utf-8">
-  <title>mozGetUserMedia Stop Audio Stream</title>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="mediaStreamPlayback.js"></script>
 </head>
 <body>
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=822109">mozGetUserMedia Stop Audio Stream</a>
-<p id="display"></p>
-<div id="content" style="display: none">
-  <audio id="testAudio"></video>
-</div>
 <pre id="test">
 <script type="application/javascript">
+  createHTML({ title: "getUserMedia Stop Audio Stream", bug: "822109" });
   /**
    * Run a test to verify that we can start an audio stream in a media element,
    * call stop() on the stream, and successfully get an ended event fired.
    */
   runTest(function () {
-    getUserMedia({audio: true}, function(stream) {
-      var testAudio = document.getElementById('testAudio');
-      var audioStreamPlayback = new LocalMediaStreamPlayback(testAudio, stream);
+    getUserMedia({audio: true})
+      .then(stream => {
+        var testAudio = createMediaElement('audio', 'testAudio');
+        var streamPlayback = new LocalMediaStreamPlayback(testAudio, stream);
 
-      audioStreamPlayback.playMediaWithStreamStop(false, SimpleTest.finish,
-        generateErrorCallback());
-    }, generateErrorCallback());
+        return streamPlayback.playMediaWithStreamStop(false);
+      })
+      .then(() => SimpleTest.finish(), generateErrorCallback());
   });
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_getUserMedia_stopAudioStreamWithFollowupAudio.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_stopAudioStreamWithFollowupAudio.html
@@ -1,51 +1,36 @@
-<!DOCTYPE HTML>
+<!DOCTYPE HTML>
 <html>
-<!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=822109
--->
 <head>
-  <meta charset="utf-8">
-  <title>mozGetUserMedia Stop Audio Stream With Followup Audio</title>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="mediaStreamPlayback.js"></script>
 </head>
 <body>
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=822109">mozGetUserMedia Stop Audio Stream With Followup Audio</a>
-<p id="display"></p>
-<div id="content" style="display: none">
-  <audio id="testAudio"></audio>
-</div>
 <pre id="test">
 <script type="application/javascript">
+  createHTML({ title: "getUserMedia Stop Audio Stream With Followup Audio", bug: "822109" });
   /**
    * Run a test to verify that I can complete an audio gum playback in a media
    * element, stop the stream, and then complete another audio gum playback
    * in a media element.
    */
   runTest(function () {
-    getUserMedia({audio: true}, function(firstStream) {
-      var testAudio = document.getElementById('testAudio');
-      var streamPlayback = new LocalMediaStreamPlayback(testAudio, firstStream);
-
-      streamPlayback.playMediaWithStreamStop(false, function() {
-        getUserMedia({audio: true}, function(secondStream) {
-          streamPlayback.mediaStream = secondStream;
+    getUserMedia({audio: true})
+      .then(firstStream => {
+        var testAudio = createMediaElement('audio', 'testAudio');
+        var streamPlayback = new LocalMediaStreamPlayback(testAudio, firstStream);
 
-          streamPlayback.playMedia(false, function() {
-            secondStream.stop();
-            SimpleTest.finish();
-          }, generateErrorCallback());
+        return streamPlayback.playMediaWithStreamStop(false)
+          .then(() => getUserMedia({audio: true}))
+          .then(secondStream => {
+            streamPlayback.mediaStream = secondStream;
 
-        }, generateErrorCallback());
-
-      }, generateErrorCallback());
-
-    }, generateErrorCallback());
+            return streamPlayback.playMedia(false)
+              .then(() => secondStream.stop());
+          });
+      })
+      .then(() => SimpleTest.finish(), generateErrorCallback());
   });
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_getUserMedia_stopVideoAudioStream.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_stopVideoAudioStream.html
@@ -1,40 +1,29 @@
 <!DOCTYPE HTML>
 <html>
-<!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=822109
--->
 <head>
-  <meta charset="utf-8">
-  <title>mozGetUserMedia Stop Video Audio Stream</title>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="mediaStreamPlayback.js"></script>
 </head>
 <body>
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=822109">mozGetUserMedia Stop Video Audio Stream</a>
-<p id="display"></p>
-<div id="content" style="display: none">
-  <video id="testVideo"></video>
-</div>
 <pre id="test">
 <script type="application/javascript">
+  createHTML({ title: "getUserMedia Stop Video Audio Stream", bug: "822109" });
   /**
    * Run a test to verify that we can start a video+audio stream in a
    * media element, call stop() on the stream, and successfully get an
    * ended event fired.
    */
   runTest(function () {
-    getUserMedia({video: true, audio: true}, function(stream) {
-      var testVideo = document.getElementById('testVideo');
-      var streamPlayback = new LocalMediaStreamPlayback(testVideo, stream);
+    getUserMedia({video: true, audio: true})
+      .then(stream => {
+        var testVideo = createMediaElement('video', 'testVideo');
+        var playback = new LocalMediaStreamPlayback(testVideo, stream);
 
-      streamPlayback.playMediaWithStreamStop(false, SimpleTest.finish,
-        generateErrorCallback());
-    }, generateErrorCallback());
+        return playback.playMediaWithStreamStop(false);
+      })
+      .then(() => SimpleTest.finish(), generateErrorCallback());
   });
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_getUserMedia_stopVideoAudioStreamWithFollowupVideoAudio.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_stopVideoAudioStreamWithFollowupVideoAudio.html
@@ -1,51 +1,39 @@
-<!DOCTYPE HTML>
+<!DOCTYPE HTML>
 <html>
-<!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=822109
--->
 <head>
-  <meta charset="utf-8">
-  <title>mozGetUserMedia Stop Video+Audio Stream With Followup Video+Audio</title>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="mediaStreamPlayback.js"></script>
 </head>
 <body>
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=822109">mozGetUserMedia Stop Video+Audio Stream With Followup Video+Audio</a>
-<p id="display"></p>
-<div id="content" style="display: none">
-  <video id="testVideo"></video>
-</div>
 <pre id="test">
 <script type="application/javascript">
+  createHTML({
+    title: "getUserMedia Stop Video+Audio Stream With Followup Video+Audio",
+    bug: "822109"
+  });
   /**
    * Run a test to verify that I can complete an video+audio gum playback in a
    * media element, stop the stream, and then complete another video+audio gum
    * playback in a media element.
    */
   runTest(function () {
-    getUserMedia({video: true, audio: true}, function(firstStream) {
-      var testVideo = document.getElementById('testVideo');
-      var streamPlayback = new LocalMediaStreamPlayback(testVideo, firstStream);
-
-      streamPlayback.playMediaWithStreamStop(false, function() {
-        getUserMedia({video: true, audio: true}, function(secondStream) {
-          streamPlayback.mediaStream = secondStream;
+    getUserMedia({video: true, audio: true})
+      .then(stream => {
+        var testVideo = createMediaElement('video', 'testVideo');
+        var streamPlayback = new LocalMediaStreamPlayback(testVideo, stream);
 
-          streamPlayback.playMedia(false, function() {
-            secondStream.stop();
-            SimpleTest.finish();
-          }, generateErrorCallback());
+        return streamPlayback.playMediaWithStreamStop(false)
+          .then(() => getUserMedia({video: true, audio: true}))
+          .then(secondStream => {
+            streamPlayback.mediaStream = secondStream;
 
-        }, generateErrorCallback());
-
-      }, generateErrorCallback());
-
-    }, generateErrorCallback());
+            return streamPlayback.playMedia(false)
+              .then(() => secondStream.stop());
+          });
+      })
+      .then(() => SimpleTest.finish(), generateErrorCallback());
   });
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_getUserMedia_stopVideoStream.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_stopVideoStream.html
@@ -1,39 +1,29 @@
-<!DOCTYPE HTML>
+<!DOCTYPE HTML>
 <html>
-<!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=822109
--->
 <head>
-  <meta charset="utf-8">
-  <title>mozGetUserMedia Stop Video Stream</title>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="mediaStreamPlayback.js"></script>
 </head>
 <body>
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=822109">mozGetUserMedia Stop Video Stream</a>
-<p id="display"></p>
-<div id="content" style="display: none">
-  <video id="testVideo"></video>
-</div>
 <pre id="test">
 <script type="application/javascript">
+  createHTML({ title: "getUserMedia Stop Video Stream", bug: "822109" });
   /**
-   * Run a test to verify that we can start a video stream in a media element,
-   * call stop() on the stream, and successfully get an ended event fired.
+   * Run a test to verify that we can start a video stream in a
+   * media element, call stop() on the stream, and successfully get an
+   * ended event fired.
    */
   runTest(function () {
-    getUserMedia({video: true}, function(stream) {
-      var testVideo = document.getElementById('testVideo');
-      var videoStreamPlayback = new LocalMediaStreamPlayback(testVideo, stream);
+    getUserMedia({video: true})
+      .then(stream => {
+        var testVideo = createMediaElement('video', 'testVideo');
+        var streamPlayback = new LocalMediaStreamPlayback(testVideo, stream);
 
-      videoStreamPlayback.playMediaWithStreamStop(false, SimpleTest.finish,
-        generateErrorCallback());
-    }, generateErrorCallback());
+        return streamPlayback.playMediaWithStreamStop(false);
+      })
+      .then(() => SimpleTest.finish(), generateErrorCallback());
   });
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_getUserMedia_stopVideoStreamWithFollowupVideo.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_stopVideoStreamWithFollowupVideo.html
@@ -1,52 +1,36 @@
-<!DOCTYPE HTML>
+<!DOCTYPE HTML>
 <html>
-<!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=822109
--->
 <head>
-  <meta charset="utf-8">
-  <title>mozGetUserMedia Stop Video Stream With Followup Video</title>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
-  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+  <script src="mediaStreamPlayback.js"></script>
 </head>
 <body>
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=822109">mozGetUserMedia Stop Video Stream With Followup Video</a>
-<p id="display"></p>
-<div id="content" style="display: none">
-  <video id="testVideo"></video>
-</div>
 <pre id="test">
 <script type="application/javascript">
+  createHTML({ title: "getUserMedia Stop Video Stream With Followup Video", bug: "822109" });
   /**
-   * Run a test to verify that I can complete an audio gum playback in a media
-   * element, stop the stream, and then complete another audio gum playback
-   * in a media element.
+   * Run a test to verify that I can complete an video gum playback in a
+   * media element, stop the stream, and then complete another video gum
+   * playback in a media element.
    */
   runTest(function () {
-    getUserMedia({video: true}, function(firstStream) {
-      var testVideo = document.getElementById('testVideo');
-      var streamPlayback = new LocalMediaStreamPlayback(testVideo,
-        firstStream);
-
-      streamPlayback.playMediaWithStreamStop(false, function() {
-        getUserMedia({video: true}, function(secondStream) {
-          streamPlayback.mediaStream = secondStream;
+    getUserMedia({video: true})
+      .then(stream => {
+        var testVideo = createMediaElement('video', 'testVideo');
+        var streamPlayback = new LocalMediaStreamPlayback(testVideo, stream);
 
-          streamPlayback.playMedia(false, function() {
-            secondStream.stop();
-            SimpleTest.finish();
-          }, generateErrorCallback());
+        return streamPlayback.playMediaWithStreamStop(false)
+          .then(() => getUserMedia({video: true}))
+          .then(secondStream => {
+            streamPlayback.mediaStream = secondStream;
 
-        }, generateErrorCallback());
-
-      }, generateErrorCallback());
-
-    }, generateErrorCallback());
+            return streamPlayback.playMedia(false)
+              .then(() => secondStream.stop());
+          });
+      })
+      .then(() => SimpleTest.finish(), generateErrorCallback());
   });
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_peerConnection_addCandidateInHaveLocalOffer.html
+++ b/dom/media/tests/mochitest/test_peerConnection_addCandidateInHaveLocalOffer.html
@@ -1,45 +1,37 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
-  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "784519",
     title: "addCandidate (answer) in 'have-local-offer'"
   });
 
   var test;
   runNetworkTest(function () {
     test = new PeerConnectionTest();
     test.setMediaConstraints([{audio: true}], [{audio: true}]);
     test.chain.removeAfter("PC_LOCAL_SET_LOCAL_DESCRIPTION");
 
-    test.chain.append([[
-      "PC_LOCAL_ADD_CANDIDATE",
-      function (test) {
-        test.pcLocal.addIceCandidateAndFail(
-          new mozRTCIceCandidate(
-            {candidate:"1 1 UDP 2130706431 192.168.2.1 50005 typ host",
-             sdpMLineIndex: 1}),
-          function(err) {
+    test.chain.append([
+      function PC_LOCAL_ADD_CANDIDATE(test) {
+        var candidate = new mozRTCIceCandidate(
+          {candidate:"1 1 UDP 2130706431 192.168.2.1 50005 typ host",
+           sdpMLineIndex: 1});
+        return test.pcLocal._pc.addIceCandidate(candidate).then(
+          generateErrorCallback("addIceCandidate should have failed."),
+          err => {
             is(err.name, "InvalidStateError", "Error is InvalidStateError");
-            test.next();
-          } );
-      }
-    ]]);
-
+          });
+        }
+    ]);
     test.run();
   });
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_peerConnection_addSecondAudioStream.html
+++ b/dom/media/tests/mochitest/test_peerConnection_addSecondAudioStream.html
@@ -1,102 +1,59 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
-  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "1091242",
     title: "Renegotiation: add second audio stream"
   });
 
   var test;
   runNetworkTest(function (options) {
     test = new PeerConnectionTest(options);
     test.chain.append([
-    [
-      'PC_LOCAL_SETUP_NEGOTIATION_CALLBACK',
-      function (test) {
+      function PC_LOCAL_SETUP_NEGOTIATION_CALLBACK(test) {
         test.pcLocal.onNegotiationneededFired = false;
-        test.pcLocal._pc.onnegotiationneeded = function (anEvent) {
+        test.pcLocal._pc.onnegotiationneeded = anEvent => {
           info("pcLocal.onnegotiationneeded fired");
           test.pcLocal.onNegotiationneededFired = true;
         };
-        test.next();
-      }
-    ],
-    [
-      'PC_LOCAL_ADD_SECOND_STREAM',
-      function (test) {
-        test.pcLocal.getAllUserMedia([{audio: true}], function () {
-          test.next();
+      },
+      function PC_LOCAL_ADD_SECOND_STREAM(test) {
+        return test.pcLocal.getAllUserMedia([{audio: true}]);
+      },
+      function PC_LOCAL_CREATE_NEW_OFFER(test) {
+        ok(test.pcLocal.onNegotiationneededFired, "onnegotiationneeded");
+        return test.createOffer(test.pcLocal).then(offer => {
+          test._new_offer = offer;
         });
-      }
-    ],
-    [
-      'PC_LOCAL_CREATE_NEW_OFFER',
-      function (test) {
-        ok(test.pcLocal.onNegotiationneededFired, "onnegotiationneeded");
-        test.createOffer(test.pcLocal, function (offer) {
-          test._new_offer = offer;
-          test.next();
+      },
+      function PC_LOCAL_SET_NEW_LOCAL_DESCRIPTION(test) {
+        return test.setLocalDescription(test.pcLocal, test._new_offer, HAVE_LOCAL_OFFER);
+      },
+      function PC_REMOTE_SET_NEW_REMOTE_DESCRIPTION(test) {
+        return test.setRemoteDescription(test.pcRemote, test._new_offer, HAVE_REMOTE_OFFER);
+      },
+      function PC_REMOTE_CREATE_NEW_ANSWER(test) {
+        return test.createAnswer(test.pcRemote).then(answer => {
+          test._new_answer = answer;
         });
-      }
-    ],
-    [
-      'PC_LOCAL_SET_NEW_LOCAL_DESCRIPTION',
-      function (test) {
-        test.setLocalDescription(test.pcLocal, test._new_offer, HAVE_LOCAL_OFFER, function () {
-          test.next();
-        });
+      },
+      function PC_REMOTE_SET_NEW_LOCAL_DESCRIPTION(test) {
+        return test.setLocalDescription(test.pcRemote, test._new_answer, STABLE);
+      },
+      function PC_LOCAL_SET_NEW_REMOTE_DESCRIPTION(test) {
+        return test.setRemoteDescription(test.pcLocal, test._new_answer, STABLE);
       }
-    ],
-    [
-      'PC_REMOTE_SET_NEW_REMOTE_DESCRIPTION',
-      function (test) {
-        test.setRemoteDescription(test.pcRemote, test._new_offer, HAVE_REMOTE_OFFER, function () {
-          test.next();
-        });
-      }
-    ],
-    [
-      'PC_REMOTE_CREATE_NEW_ANSWER',
-      function (test) {
-        test.createAnswer(test.pcRemote, function (answer) {
-          test._new_answer = answer;
-          test.next();
-        });
-      }
-    ],
-    [
-      'PC_REMOTE_SET_NEW_LOCAL_DESCRIPTION',
-      function (test) {
-        test.setLocalDescription(test.pcRemote, test._new_answer, STABLE, function () {
-          test.next();
-        });
-      }
-    ],
-    [
-      'PC_LOCAL_SET_NEW_REMOTE_DESCRIPTION',
-      function (test) {
-        test.setRemoteDescription(test.pcLocal, test._new_answer, STABLE, function () {
-          test.next();
-        });
-      }
-    ]
-    // TODO(bug 1093835): figure out how to verify if media flows through the new stream
+      // TODO(bug 1093835): figure out how to verify if media flows through the new stream
     ]);
     test.setMediaConstraints([{audio: true}], [{audio: true}]);
     test.run();
   });
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_peerConnection_basicAudio.html
+++ b/dom/media/tests/mochitest/test_peerConnection_basicAudio.html
@@ -1,18 +1,12 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
-  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "796892",
     title: "Basic audio-only peer connection"
   });
--- a/dom/media/tests/mochitest/test_peerConnection_basicAudioVideo.html
+++ b/dom/media/tests/mochitest/test_peerConnection_basicAudioVideo.html
@@ -1,18 +1,12 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
-  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "796890",
     title: "Basic audio/video (separate) peer connection"
   });
--- a/dom/media/tests/mochitest/test_peerConnection_basicAudioVideoCombined.html
+++ b/dom/media/tests/mochitest/test_peerConnection_basicAudioVideoCombined.html
@@ -1,18 +1,12 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
-  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "796890",
     title: "Basic audio/video (combined) peer connection"
   });
--- a/dom/media/tests/mochitest/test_peerConnection_basicAudioVideoCombined_long.html
+++ b/dom/media/tests/mochitest/test_peerConnection_basicAudioVideoCombined_long.html
@@ -1,44 +1,37 @@
 <!DOCTYPE HTML>
 
 <!-- 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/. -->
 
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="long.js"></script>
-  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "1014328",
     title: "Basic audio/video (combined) peer connection, long running",
     visible: true
   });
 
   var test;
-  runTest(function (options) {
+  runNetworkTest(function (options) {
     options = options || {};
     options.commands = commandsPeerConnection.slice(0);
     options.commands.push(generateIntervalCommand(verifyConnectionStatus,
                                                   1000 * 10,
                                                   1000 * 3600 * 3));
 
     test = new PeerConnectionTest(options);
     test.setMediaConstraints([{audio: true, video: true, fake: false}],
                              [{audio: true, video: true, fake: false}]);
     test.run();
   });
 </script>
 </pre>
 </body>
 </html>
-
--- a/dom/media/tests/mochitest/test_peerConnection_basicAudioVideoNoBundle.html
+++ b/dom/media/tests/mochitest/test_peerConnection_basicAudioVideoNoBundle.html
@@ -1,43 +1,34 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
-  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "1016476",
     title: "Basic audio/video peer connection with no Bundle"
   });
 
-  SimpleTest.requestFlakyTimeout("WebRTC is full of inherent timeouts");
-
-  var test;
-  runNetworkTest(function (options) {
-    test = new PeerConnectionTest(options);
-    test.chain.insertAfter('PC_LOCAL_CREATE_OFFER',
-    [['PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER',
-      function (test) {
-        test.originalOffer.sdp = test.originalOffer.sdp.replace(
-          /a=group:BUNDLE .*\r\n/g,
-          ""
-        );
-        info("Updated no bundle offer: " + JSON.stringify(test.originalOffer));
-        test.next();
-      }
-    ]]);
+  runNetworkTest(options => {
+    var test = new PeerConnectionTest(options);
+    test.chain.insertAfter(
+      'PC_LOCAL_CREATE_OFFER',
+      [
+        function PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER(test) {
+          test.originalOffer.sdp = test.originalOffer.sdp.replace(
+              /a=group:BUNDLE .*\r\n/g,
+            ""
+          );
+          info("Updated no bundle offer: " + JSON.stringify(test.originalOffer));
+        }
+      ]);
     test.setMediaConstraints([{audio: true}, {video: true}],
                              [{audio: true}, {video: true}]);
     test.run();
   });
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_peerConnection_basicAudio_long.html
+++ b/dom/media/tests/mochitest/test_peerConnection_basicAudio_long.html
@@ -1,24 +1,18 @@
 <!DOCTYPE HTML>
 
 <!-- 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/. -->
 
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="long.js"></script>
-  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "796892",
     title: "Basic audio-only peer connection",
     visible: true
--- a/dom/media/tests/mochitest/test_peerConnection_basicH264Video.html
+++ b/dom/media/tests/mochitest/test_peerConnection_basicH264Video.html
@@ -1,18 +1,12 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
-  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript;version=1.8">
   createHTML({
     bug: "1040346",
     title: "Basic H.264 GMP video-only peer connection"
   });
--- a/dom/media/tests/mochitest/test_peerConnection_basicScreenshare.html
+++ b/dom/media/tests/mochitest/test_peerConnection_basicScreenshare.html
@@ -1,18 +1,12 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
-  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "1039666",
     title: "Basic screenshare-only peer connection"
   });
--- a/dom/media/tests/mochitest/test_peerConnection_basicVideo.html
+++ b/dom/media/tests/mochitest/test_peerConnection_basicVideo.html
@@ -1,18 +1,12 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
-  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "796888",
     title: "Basic video-only peer connection"
   });
--- a/dom/media/tests/mochitest/test_peerConnection_basicVideo_long.html
+++ b/dom/media/tests/mochitest/test_peerConnection_basicVideo_long.html
@@ -1,24 +1,18 @@
 <!DOCTYPE HTML>
 
 <!-- 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/. -->
 
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="long.js"></script>
-  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "796888",
     title: "Basic video-only peer connection",
     visible: true
--- a/dom/media/tests/mochitest/test_peerConnection_basicWindowshare.html
+++ b/dom/media/tests/mochitest/test_peerConnection_basicWindowshare.html
@@ -1,18 +1,12 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
-  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "1038926",
     title: "Basic windowshare-only peer connection"
   });
--- a/dom/media/tests/mochitest/test_peerConnection_bug1013809.html
+++ b/dom/media/tests/mochitest/test_peerConnection_bug1013809.html
@@ -1,17 +1,12 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "1013809",
     title: "Audio-only peer connection with swapped setLocal and setRemote steps"
   });
--- a/dom/media/tests/mochitest/test_peerConnection_bug1042791.html
+++ b/dom/media/tests/mochitest/test_peerConnection_bug1042791.html
@@ -1,18 +1,12 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
-  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript;version=1.8">
   createHTML({
     bug: "1040346",
     title: "Basic H.264 GMP video-only peer connection"
   });
@@ -20,25 +14,23 @@
   var test;
   runNetworkTest(function (options) {
     options = options || { };
     options.h264 = true;
     test = new PeerConnectionTest(options);
     test.setMediaConstraints([{video: true}], [{video: true}]);
     test.chain.removeAfter("PC_LOCAL_CREATE_OFFER");
 
-    test.chain.append([[
-      "PC_LOCAL_VERIFY_H264_OFFER",
-      function (test) {
+    test.chain.append([
+      function PC_LOCAL_VERIFY_H264_OFFER(test) {
         ok(!test.pcLocal._latest_offer.sdp.toLowerCase().contains("profile-level-id=0x42e0"),
-          "H264 offer does not contain profile-level-id=0x42e0");
+           "H264 offer does not contain profile-level-id=0x42e0");
         ok(test.pcLocal._latest_offer.sdp.toLowerCase().contains("profile-level-id=42e0"),
-          "H264 offer contains profile-level-id=42e0");
-        test.next();
+           "H264 offer contains profile-level-id=42e0");
       }
-    ]]);
+    ]);
 
     test.run();
   });
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_peerConnection_bug822674.html
+++ b/dom/media/tests/mochitest/test_peerConnection_bug822674.html
@@ -1,14 +1,11 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="pc.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "822674",
     title: "mozRTCPeerConnection isn't a true javascript object as it should be"
--- a/dom/media/tests/mochitest/test_peerConnection_bug825703.html
+++ b/dom/media/tests/mochitest/test_peerConnection_bug825703.html
@@ -1,83 +1,80 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="pc.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "825703",
     title: "RTCConfiguration valid/invalid permutations"
   });
 
-  makePC = (config, expected_error) => {
-    var exception;
-    try {
-      new mozRTCPeerConnection(config).close();
-    } catch (e) {
-      exception = e;
-    }
-    is((exception? exception.name : "success"), expected_error || "success",
-       "mozRTCPeerConnection(" + JSON.stringify(config) + ")");
+var makePC = (config, expected_error) => {
+  var exception;
+  try {
+    new mozRTCPeerConnection(config).close();
+  } catch (e) {
+    exception = e;
+  }
+  is((exception? exception.name : "success"), expected_error || "success",
+     "mozRTCPeerConnection(" + JSON.stringify(config) + ")");
+};
+
+// This is a test of the iceServers parsing code + readable errors
+runNetworkTest(() => {
+  var exception = null;
+
+  try {
+    new mozRTCPeerConnection().close();
+  } catch (e) {
+    exception = e;
+  }
+  ok(!exception, "mozRTCPeerConnection() succeeds");
+  exception = null;
+
+  makePC();
+
+  makePC(1, "TypeError");
+
+  makePC({});
+
+  makePC({ iceServers: [] });
+
+  makePC({ iceServers: [{ urls:"" }] }, "SyntaxError");
+
+  makePC({ iceServers: [
+    { urls:"stun:127.0.0.1" },
+    { urls:"stun:localhost", foo:"" },
+    { urls: ["stun:127.0.0.1", "stun:localhost"] },
+    { urls:"stuns:localhost", foo:"" },
+    { urls:"turn:[::1]:3478", username:"p", credential:"p" },
+    { urls:"turn:localhost:3478?transport=udp", username:"p", credential:"p" },
+    { urls: ["turn:[::1]:3478", "turn:localhost"], username:"p", credential:"p" },
+    { urls:"turns:localhost:3478?transport=udp", username:"p", credential:"p" },
+    { url:"stun:localhost", foo:"" },
+    { url:"turn:localhost", username:"p", credential:"p" }
+  ]});
+
+  makePC({ iceServers: [{ urls: ["stun:127.0.0.1", ""] }] }, "SyntaxError");
+
+  makePC({ iceServers: [{ urls:"turns:localhost:3478", username:"p" }] }, "InvalidAccessError");
+
+  makePC({ iceServers: [{ url:"turns:localhost:3478", credential:"p" }] }, "InvalidAccessError");
+
+  makePC({ iceServers: [{ urls:"http:0.0.0.0" }] }, "SyntaxError");
+
+  try {
+    new mozRTCPeerConnection({ iceServers: [{ url:"http:0.0.0.0" }] }).close();
+  } catch (e) {
+    ok(e.message.indexOf("http") > 0,
+       "mozRTCPeerConnection() constructor has readable exceptions");
   }
 
-  // This is a test of the iceServers parsing code + readable errors
-
-  runNetworkTest(function () {
-    var exception = null;
-
-    try {
-      new mozRTCPeerConnection().close();
-    } catch (e) {
-      exception = e;
-    }
-    ok(!exception, "mozRTCPeerConnection() succeeds");
-    exception = null;
-
-    makePC();
-
-    makePC(1, "TypeError");
-
-    makePC({});
-
-    makePC({ iceServers: [] });
-
-    makePC({ iceServers: [{ urls:"" }] }, "SyntaxError");
-
-    makePC({ iceServers: [
-      { urls:"stun:127.0.0.1" },
-      { urls:"stun:localhost", foo:"" },
-      { urls: ["stun:127.0.0.1", "stun:localhost"] },
-      { urls:"stuns:localhost", foo:"" },
-      { urls:"turn:[::1]:3478", username:"p", credential:"p" },
-      { urls:"turn:localhost:3478?transport=udp", username:"p", credential:"p" },
-      { urls: ["turn:[::1]:3478", "turn:localhost"], username:"p", credential:"p" },
-      { urls:"turns:localhost:3478?transport=udp", username:"p", credential:"p" },
-      { url:"stun:localhost", foo:"" },
-      { url:"turn:localhost", username:"p", credential:"p" }
-    ]});
-
-    makePC({ iceServers: [{ urls: ["stun:127.0.0.1", ""] }] }, "SyntaxError");
-
-    makePC({ iceServers: [{ urls:"turns:localhost:3478", username:"p" }] }, "InvalidAccessError");
-
-    makePC({ iceServers: [{ url:"turns:localhost:3478", credential:"p" }] }, "InvalidAccessError");
-
-    makePC({ iceServers: [{ urls:"http:0.0.0.0" }] }, "SyntaxError");
-    try {
-      new mozRTCPeerConnection({ iceServers: [{ url:"http:0.0.0.0" }] }).close();
-    } catch (e) {
-      ok(e.message.indexOf("http") > 0,
-         "mozRTCPeerConnection() constructor has readable exceptions");
-    }
-
-    networkTestFinished();
-  });
+  networkTestFinished();
+});
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_peerConnection_bug827843.html
+++ b/dom/media/tests/mochitest/test_peerConnection_bug827843.html
@@ -1,72 +1,70 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "827843",
     title: "Ensure that localDescription and remoteDescription are null after close"
   });
 
-  var steps = [
-    [
-      "CHECK_SDP_ON_CLOSED_PC",
-      function (test) {
-        var description;
-        var exception = null;
+var steps = [
+  function CHECK_SDP_ON_CLOSED_PC(test) {
+    var description;
+    var exception = null;
 
-        // handle the event which the close() triggers
-        test.pcLocal.onsignalingstatechange = function (e) {
-          is(e.target.signalingState, "closed",
-             "Received expected onsignalingstatechange event on 'closed'");
-        }
-
-        test.pcLocal.close();
+    // handle the event which the close() triggers
+    var localClosed = new Promise(resolve => {
+      test.pcLocal.onsignalingstatechange = e => {
+        is(e.target.signalingState, "closed",
+           "Received expected onsignalingstatechange event on 'closed'");
+        resolve();
+      }
+    });
 
-        try { description = test.pcLocal.localDescription; } catch (e) { exception = e; }
-        ok(exception, "Attempt to access localDescription of pcLocal after close throws exception");
-        exception = null;
+    test.pcLocal.close();
 
-        try { description = test.pcLocal.remoteDescription; } catch (e) { exception = e; }
-        ok(exception, "Attempt to access remoteDescription of pcLocal after close throws exception");
-        exception = null;
+    try { description = test.pcLocal.localDescription; } catch (e) { exception = e; }
+    ok(exception, "Attempt to access localDescription of pcLocal after close throws exception");
+    exception = null;
+
+    try { description = test.pcLocal.remoteDescription; } catch (e) { exception = e; }
+    ok(exception, "Attempt to access remoteDescription of pcLocal after close throws exception");
+    exception = null;
 
-        // handle the event which the close() triggers
-        test.pcRemote.onsignalingstatechange = function (e) {
-          is(e.target.signalingState, "closed",
-             "Received expected onsignalingstatechange event on 'closed'");
-        }
+    // handle the event which the close() triggers
+    var remoteClosed = new Promise(resolve => {
+      test.pcRemote.onsignalingstatechange = e => {
+        is(e.target.signalingState, "closed",
+           "Received expected onsignalingstatechange event on 'closed'");
+        resolve();
+      }
+    });
 
-        test.pcRemote.close();
+    test.pcRemote.close();
 
-        try  { description = test.pcRemote.localDescription; } catch (e) { exception = e; }
-        ok(exception, "Attempt to access localDescription of pcRemote after close throws exception");
-        exception = null;
+    try  { description = test.pcRemote.localDescription; } catch (e) { exception = e; }
+    ok(exception, "Attempt to access localDescription of pcRemote after close throws exception");
+    exception = null;
 
-        try  { description = test.pcRemote.remoteDescription; } catch (e) { exception = e; }
-        ok(exception, "Attempt to access remoteDescription of pcRemote after close throws exception");
+    try  { description = test.pcRemote.remoteDescription; } catch (e) { exception = e; }
+    ok(exception, "Attempt to access remoteDescription of pcRemote after close throws exception");
 
-        test.next();
-      }
-    ]
-  ];
+    return Promise.all([localClosed, remoteClosed]);
+  }
+];
 
-  var test;
-  runNetworkTest(function () {
-    test = new PeerConnectionTest();
-    test.setMediaConstraints([{audio: true}], [{audio: true}]);
-    test.chain.append(steps);
-    test.run();
-  });
+var test;
+runNetworkTest(() => {
+  test = new PeerConnectionTest();
+  test.setMediaConstraints([{audio: true}], [{audio: true}]);
+  test.chain.append(steps);
+  test.run();
+});
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_peerConnection_bug834153.html
+++ b/dom/media/tests/mochitest/test_peerConnection_bug834153.html
@@ -1,14 +1,11 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="pc.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "834153",
     title: "Queue CreateAnswer in PeerConnection.js"
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_callbacks.html
@@ -0,0 +1,92 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript;version=1.8">
+  createHTML({
+    title: "PeerConnection using callback functions",
+    bug: "1119593",
+    visible: true
+  });
+
+// This still aggressively uses promises, but it is testing that the callback functions
+// are properly in place.
+
+// wrapper that turns a callback-based function call into a promise
+function pcall(o, f, beforeArg) {
+  return new Promise((resolve, reject) => {
+    var args = [resolve, reject];
+    if (typeof beforeArg !== 'undefined') {
+      args.unshift(beforeArg);
+    }
+    info('Calling ' + f.name);
+    f.apply(o, args);
+  });
+}
+
+var pc1 = new mozRTCPeerConnection();
+var pc2 = new mozRTCPeerConnection();
+
+var pc2_haveRemoteOffer = new Promise(resolve => {
+  pc2.onsignalingstatechange =
+    e => (e.target.signalingState == "have-remote-offer") && resolve();
+});
+var pc1_stable = new Promise(resolve => {
+  pc1.onsignalingstatechange =
+    e => (e.target.signalingState == "stable") && resolve();
+});
+
+pc1.onicecandidate = e => {
+  pc2_haveRemoteOffer
+    .then(() => !e.candidate || pcall(pc2, pc2.addIceCandidate, e.candidate))
+    .catch(generateErrorCallback());
+};
+pc2.onicecandidate = e => {
+  pc1_stable
+    .then(() => !e.candidate || pcall(pc1, pc1.addIceCandidate, e.candidate))
+    .catch(generateErrorCallback());
+};
+
+var v1, v2;
+var delivered = new Promise(resolve => {
+  pc2.onaddstream = e => {
+    v2.mozSrcObject = e.stream;
+    resolve(e.stream);
+  };
+});
+
+runNetworkTest(function() {
+  v1 = createMediaElement('video', 'v1');
+  v2 = createMediaElement('video', 'v2');
+  var canPlayThrough = new Promise(resolve => v2.canplaythrough = resolve);
+  is(v2.currentTime, 0, "v2.currentTime is zero at outset");
+
+  // not testing legacy gUM here
+  navigator.mediaDevices.getUserMedia({ fake: true, video: true, audio: true })
+    .then(stream => pc1.addStream(v1.mozSrcObject = stream))
+    .then(() => pcall(pc1, pc1.createOffer))
+    .then(offer => pcall(pc1, pc1.setLocalDescription, offer))
+    .then(() => pcall(pc2, pc2.setRemoteDescription, pc1.localDescription))
+    .then(() => pcall(pc2, pc2.createAnswer))
+    .then(answer => pcall(pc2, pc2.setLocalDescription, answer))
+    .then(() => pcall(pc1, pc1.setRemoteDescription, pc2.localDescription))
+    .then(() => delivered)
+  //    .then(() => canPlayThrough)    // why doesn't this fire?
+    .then(() => waitUntil(() => v2.currentTime > 0 && v2.mozSrcObject.currentTime > 0))
+    .then(() => ok(v2.currentTime > 0, "v2.currentTime is moving (" + v2.currentTime + ")"))
+    .then(() => ok(true, "Connected."))
+    .then(() => pcall(pc1, pc1.getStats, null))
+    .then(stats => ok(Object.keys(stats).length > 0, "pc1 has stats"))
+    .then(() => pcall(pc2, pc2.getStats, null))
+    .then(stats => ok(Object.keys(stats).length > 0, "pc2 has stats"))
+    .then(() => { v1.pause(); v2.pause(); })
+    .catch(reason => ok(false, "unexpected failure: " + reason))
+      .then(networkTestFinished);
+});
+</script>
+</pre>
+</body>
+</html>
--- a/dom/media/tests/mochitest/test_peerConnection_capturedVideo.html
+++ b/dom/media/tests/mochitest/test_peerConnection_capturedVideo.html
@@ -1,55 +1,45 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
-  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <video id="v1" src="../../test/vp9cake.webm" height="120" width="160" autoplay muted></video>
 <pre id="test">
 <script type="application/javascript;version=1.8">
   createHTML({
     bug: "1081409",
     title: "Captured video-only over peer connection",
     visible: true
   });
 
-  var metadataLoaded = new Promise(resolve => {
-    if (v1.readyState < v1.HAVE_METADATA) {
-      v1.onloadedmetadata = e => resolve();
-      return;
-    }
+var metadataLoaded = new Promise(resolve => {
+  if (v1.readyState < v1.HAVE_METADATA) {
+    v1.onloadedmetadata = resolve;
+  } else {
     resolve();
-  });
+  }
+});
 
-  runNetworkTest(function() {
-    var test = new PeerConnectionTest();
-    test.setOfferOptions({ offerToReceiveVideo: false,
-                           offerToReceiveAudio: false });
-    test.chain.insertAfter("PC_LOCAL_GUM", [["PC_LOCAL_CAPTUREVIDEO", function (test) {
-      metadataLoaded
-      .then(function() {
-        var stream = v1.mozCaptureStreamUntilEnded();
-        is(stream.getTracks().length, 2, "Captured stream has 2 tracks");
-        stream.getTracks().forEach(tr => test.pcLocal._pc.addTrack(tr, stream));
-        test.pcLocal.constraints = [{ video: true, audio:true }]; // fool tests
-        test.next();
-      })
-      .catch(function(reason) {
-        ok(false, "unexpected failure: " + reason);
-        SimpleTest.finish();
-      });
+runNetworkTest(function() {
+  var test = new PeerConnectionTest();
+  test.setOfferOptions({ offerToReceiveVideo: false,
+                         offerToReceiveAudio: false });
+  test.chain.insertAfter("PC_LOCAL_GUM", [
+    function PC_LOCAL_CAPTUREVIDEO(test) {
+      return metadataLoaded
+        .then(() => {
+          var stream = v1.mozCaptureStreamUntilEnded();
+          is(stream.getTracks().length, 2, "Captured stream has 2 tracks");
+          stream.getTracks().forEach(tr => test.pcLocal._pc.addTrack(tr, stream));
+          test.pcLocal.constraints = [{ video: true, audio:true }]; // fool tests
+        });
     }
-    ]]);
-    test.chain.removeAfter("PC_REMOTE_CHECK_MEDIA_FLOW_PRESENT");
-    test.run();
-  });
+  ]);
+  test.chain.removeAfter("PC_REMOTE_CHECK_MEDIA_FLOW_PRESENT");
+  test.run();
+});
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_peerConnection_close.html
+++ b/dom/media/tests/mochitest/test_peerConnection_close.html
@@ -1,14 +1,11 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="pc.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "991877",
     title: "Basic RTCPeerConnection.close() tests"
--- a/dom/media/tests/mochitest/test_peerConnection_errorCallbacks.html
+++ b/dom/media/tests/mochitest/test_peerConnection_errorCallbacks.html
@@ -1,14 +1,11 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="pc.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "834270",
     title: "Align PeerConnection error handling with WebRTC specification"
--- a/dom/media/tests/mochitest/test_peerConnection_noTrickleAnswer.html
+++ b/dom/media/tests/mochitest/test_peerConnection_noTrickleAnswer.html
@@ -1,19 +1,13 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
-  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
   <script type="application/javascript" src="nonTrickleIce.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "1060102",
     title: "Basic audio only SDP answer without trickle ICE"
   });
--- a/dom/media/tests/mochitest/test_peerConnection_noTrickleOffer.html
+++ b/dom/media/tests/mochitest/test_peerConnection_noTrickleOffer.html
@@ -1,19 +1,13 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
-  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
   <script type="application/javascript" src="nonTrickleIce.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "1060102",
     title: "Basic audio only SDP offer without trickle ICE"
   });
--- a/dom/media/tests/mochitest/test_peerConnection_noTrickleOfferAnswer.html
+++ b/dom/media/tests/mochitest/test_peerConnection_noTrickleOfferAnswer.html
@@ -1,19 +1,13 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
-  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
   <script type="application/javascript" src="nonTrickleIce.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "1060102",
     title: "Basic audio only SDP offer and answer without trickle ICE"
   });
--- a/dom/media/tests/mochitest/test_peerConnection_offerRequiresReceiveAudio.html
+++ b/dom/media/tests/mochitest/test_peerConnection_offerRequiresReceiveAudio.html
@@ -1,17 +1,12 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "850275",
     title: "Simple offer media constraint test with audio"
   });
--- a/dom/media/tests/mochitest/test_peerConnection_offerRequiresReceiveVideo.html
+++ b/dom/media/tests/mochitest/test_peerConnection_offerRequiresReceiveVideo.html
@@ -1,17 +1,12 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "850275",
     title: "Simple offer media constraint test with video"
   });
--- a/dom/media/tests/mochitest/test_peerConnection_offerRequiresReceiveVideoAudio.html
+++ b/dom/media/tests/mochitest/test_peerConnection_offerRequiresReceiveVideoAudio.html
@@ -1,17 +1,12 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="templates.js"></script>
-  <script type="application/javascript" src="turnConfig.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "850275",
     title: "Simple offer media constraint test with video/audio"
   });
--- a/dom/media/tests/mochitest/test_peerConnection_promiseSendOnly.html
+++ b/dom/media/tests/mochitest/test_peerConnection_promiseSendOnly.html
@@ -1,62 +1,57 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="application/javascript" src="head.js"></script>
   <script type="application/javascript" src="pc.js"></script>
 </head>
 <body>
-<video id="v1" controls="controls" height="120" width="160" autoplay></video>
-<video id="v2" controls="controls" height="120" width="160" autoplay></video><br>
 <pre id="test">
 <script type="application/javascript;version=1.8">
   createHTML({
     bug: "1091898",
     title: "PeerConnection with promises (sendonly)",
     visible: true
   });