Bug 1119593 - Fixing test preconditions for steeplechase, r=drno
authorMartin Thomson <martin.thomson@gmail.com>
Wed, 28 Jan 2015 14:05:57 -0800
changeset 226573 a18af75beb27b0079addf7d4593b7b7eb3a0dd28
parent 226572 75f409f3a7b1bf16faf3e8c4b25ca0419c4f10a1
child 226574 7f03823a577ee8ceb412644903df352ae68f9124
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)
reviewersdrno
bugs1119593
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
Bug 1119593 - Fixing test preconditions for steeplechase, r=drno
dom/media/tests/mochitest/dataChannel.js
dom/media/tests/mochitest/head.js
dom/media/tests/mochitest/mediaStreamPlayback.js
dom/media/tests/mochitest/network.js
dom/media/tests/mochitest/pc.js
dom/media/tests/mochitest/steeplechase.ini
dom/media/tests/mochitest/templates.js
--- a/dom/media/tests/mochitest/dataChannel.js
+++ b/dom/media/tests/mochitest/dataChannel.js
@@ -6,22 +6,18 @@
  * 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 = function (event) {
-      resolve(event.target.result);
-    };
-
+    reader.onloadend = event => resolve(event.target.result);
     reader.readAsText(blob);
   });
 }
 
 function addInitialDataChannel(chain) {
   chain.insertBefore('PC_LOCAL_CREATE_OFFER', [
     function PC_REMOTE_EXPECT_DATA_CHANNEL(test) {
       test.pcRemote.expectDataChannel();
@@ -34,27 +30,21 @@ function addInitialDataChannel(chain) {
 
       is(test.pcLocal.signalingState, STABLE,
          "Create datachannel does not change signaling state");
     }
   ]);
 
   chain.insertBefore('PC_LOCAL_CHECK_MEDIA_TRACKS', [
     function PC_LOCAL_VERIFY_DATA_CHANNEL_STATE(test) {
-      return test.pcLocal.dataChannels[0].opened
-        .then(() =>
-              ok(true, test.pcLocal + " dataChannels[0] switched to 'open'"));
+      return test.pcLocal.dataChannels[0].opened;
     },
 
     function PC_REMOTE_VERIFY_DATA_CHANNEL_STATE(test) {
-      return test.pcRemote.nextDataChannel
-        .then(channel => channel.opened)
-        .then(channel =>
-              is(channel.readyState, "open",
-                 test.pcRemote + " dataChannels[0] switched to 'open'"));
+      return test.pcRemote.nextDataChannel.then(channel => channel.opened);
     }
   ]);
   chain.removeAfter('PC_REMOTE_CHECK_ICE_CONNECTIONS');
   chain.append([
     function SEND_MESSAGE(test) {
       var message = "Lorem ipsum dolor sit amet";
 
       return test.send(message).then(result => {
@@ -164,17 +154,17 @@ function addInitialDataChannel(chain) {
           is(targetChannel2.maxRetransmitTime, options.maxRetransmitTime, targetChannel2 + " maxRetransmitTime is:" +
           targetChannel2.maxRetransmitTime);
         */
       });
     },
 
     function SEND_MESSAGE_THROUGH_LAST_OPENED_CHANNEL2(test) {
       var channels = test.pcRemote.dataChannels;
-      var message = "I am the walrus; Goo goo g' joob";
+      var message = "I am the walrus; Goo goo g'joob";
 
       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
@@ -109,61 +109,72 @@ function getUserMedia(constraints) {
   if (!("fake" in constraints) && FAKE_ENABLED) {
     constraints["fake"] = FAKE_ENABLED;
   }
 
   info("Call getUserMedia for " + JSON.stringify(constraints));
   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 realRunTest(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
@@ -249,17 +260,17 @@ function generateErrorCallback(message) 
       ok(false, "Unexpected callback with message = '" + message +
          "' at: " + JSON.stringify(stack));
     }
     throw new Error("Unexpected callback");
   }
 }
 
 var unexpectedEventArrived;
-var unexpectedEventArrivedPromise = new Promise((x, reject) => {
+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
@@ -274,42 +285,37 @@ function unexpectedEvent(message, eventN
     var details = "Unexpected event '" + eventName + "' fired with message = '" +
         message + "' at: " + JSON.stringify(stack);
     ok(false, details);
     unexpectedEventArrived(new Error(details));
   }
 }
 
 /**
- * Implements the event guard pattern used throughout.  Each of the 'onxxx'
+ * 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.
- * Once but that handler is used exactly once, and subsequent events will also
- * cause 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
- * @param {function} redirect
- *        (Optional) a function that determines what is passed to the event
- *        handler. By default, the handler is passed the wrapper (as opposed to
- *        the normal cases where they receive an event object).  This redirect
- *        function is passed the event.
  */
-function guardEvent(wrapper, obj, event, redirect) {
-  redirect = redirect || (e => wrapper);
+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');
-    wrapper[onx](redirect(e));
+    e.wrapper = wrapper;
+    wrapper[onx](e);
     wrapper[onx] = unexpected;
   };
 }
 
 
 /**
  * This class executes a series of functions in a continuous sequence.
  * Promise-bearing functions are executed after the previous promise completes.
@@ -334,198 +340,147 @@ CommandChain.prototype = {
   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),
-          unexpectedEventArrivedPromise
-        ]);
+        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
-   *
-   * @param {function[]} commands
-   *        List of command functions
    */
   append: function(commands) {
     this.commands = this.commands.concat(commands);
   },
 
   /**
    * Returns the index of the specified command in the chain.
-   *
-   * @param {function|string} id
-   *        Command function or name
-   * @returns {number} Index of the command
    */
-  indexOf: function (id) {
-    if (typeof id === 'string') {
-      return this.commands.findIndex(f => f.name === id);
+  indexOf: function(functionOrName) {
+    if (typeof functionOrName === 'string') {
+      return this.commands.findIndex(f => f.name === functionOrName);
     }
-    return this.commands.indexOf(id);
+    return this.commands.indexOf(functionOrName);
   },
 
   /**
    * Inserts the new commands after the specified command.
-   *
-   * @param {function|string} id
-   *        Command function or name
-   * @param {function[]} commands
-   *        List of commands
    */
-  insertAfter: function (id, commands) {
-    this._insertHelper(id, commands, 1);
+  insertAfter: function(functionOrName, commands) {
+    this._insertHelper(functionOrName, commands, 1);
   },
 
   /**
    * Inserts the new commands before the specified command.
-   *
-   * @param {string} id
-   *        Command function or name
-   * @param {function[]} commands
-   *        List of commands
    */
-  insertBefore: function (id, commands) {
-    this._insertHelper(id, commands);
+  insertBefore: function(functionOrName, commands) {
+    this._insertHelper(functionOrName, commands, 0);
   },
 
-  _insertHelper: function(id, commands, delta) {
-    delta = delta || 0;
-    var index = this.indexOf(id);
+  _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
-   *
-   * @param {function|string} id
-   *         Command function or name
-   * @returns {function[]} Removed commands
+   * Removes the specified command, returns what was removed.
    */
-  remove: function (id) {
-    var index = this.indexOf(id);
+  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.
-   *
-   * @param {function|string} id
-   *        Command function or name
-   * @returns {function[]} Removed commands
+   * Removes all commands after the specified one, returns what was removed.
    */
-  removeAfter: function (id) {
-    var index = this.indexOf(id);
+  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.
-   *
-   * @param {function|string} id
-   *        Command function or name
-   * @returns {function[]} Removed commands
+   * Removes all commands before the specified one, returns what was removed.
    */
-  removeBefore: function (id) {
-    var index = this.indexOf(id);
+  removeBefore: function(functionOrName) {
+    var index = this.indexOf(functionOrName);
     if (index >= 0) {
       return this.commands.splice(0, index);
     }
     return [];
   },
 
   /**
-   * Replaces a single command.
-   *
-   * @param {function|string} id
-   *        Command function or name
-   * @param {function[]} commands
-   *        List of commands
-   * @returns {function[]} Removed commands
+   * Replaces a single command, returns what was removed.
    */
-  replace: function (id, commands) {
-    this.insertBefore(id, commands);
-    return this.remove(id);
+  replace: function(functionOrName, commands) {
+    this.insertBefore(functionOrName, commands);
+    return this.remove(functionOrName);
   },
 
   /**
-   * Replaces all commands after the specified one.
-   *
-   * @param {function|string} id
-   *        Command function or name
-   * @returns {object[]} Removed commands
+   * Replaces all commands after the specified one, returns what was removed.
    */
-  replaceAfter: function (id, commands) {
-    var oldCommands = this.removeAfter(id);
+  replaceAfter: function(functionOrName, commands) {
+    var oldCommands = this.removeAfter(functionOrName);
     this.append(commands);
     return oldCommands;
   },
 
   /**
-   * Replaces all commands before the specified one.
-   *
-   * @param {function|string} id
-   *        Command function or name
-   * @returns {object[]} Removed commands
+   * Replaces all commands before the specified one, returns what was removed.
    */
-  replaceBefore: function (id, commands) {
-    var oldCommands = this.removeBefore(id);
-    this.insertBefore(id, commands);
+  replaceBefore: function(functionOrName, commands) {
+    var oldCommands = this.removeBefore(functionOrName);
+    this.insertBefore(functionOrName, commands);
     return oldCommands;
   },
 
   /**
    * Remove all commands whose name match the specified regex.
-   *
-   * @param {regex} id_match
-   *        Regular expression to match command names.
    */
   filterOut: function (id_match) {
     this.commands = this.commands.filter(c => !id_match.test(c.name));
   }
 };
 
 
 function IsMacOSX10_6orOlder() {
-    var is106orOlder = false;
+  if (navigator.platform.indexOf("Mac") !== 0) {
+    return false;
+  }
 
-    if (navigator.platform.indexOf("Mac") == 0) {
-        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.
-        is106orOlder = (parseFloat(version) < 11.0);
-    }
-    return is106orOlder;
+  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/mediaStreamPlayback.js
+++ b/dom/media/tests/mochitest/mediaStreamPlayback.js
@@ -209,31 +209,28 @@ LocalMediaStreamPlayback.prototype = Obj
         setTimeout(() => {
           reject(new Error("ended event never fired"));
         }, ENDED_TIMEOUT_LENGTH);
       });
     }
   }
 });
 
-function runTest(f) {
-  // Use addEventListener to avoid SimpleTest hacking an .onload assignment
-  window.addEventListener('load', () => {
-    SimpleTest.waitForExplicitFinish();
-    realRunTest(f);
-  });
-}
-
-function createHTML(options) {
-  window.addEventListener('load', () => {
-    realCreateHTML(options);
-  });
-}
-
-[
+var scriptsReady = Promise.all([
   "/tests/SimpleTest/SimpleTest.js",
   "head.js"
-].forEach(script => {
-  console.log('msp');
+].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(() => {
+    SimpleTest.waitForExplicitFinish();
+    return runTestWhenReady(f);
+  });
+}
--- a/dom/media/tests/mochitest/network.js
+++ b/dom/media/tests/mochitest/network.js
@@ -25,26 +25,26 @@ function isNetworkReady() {
       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);
+          info("Network interface is ready with address: " + ip);
           return true;
         }
       }
     }
     // ip address is not available
-    safeInfo("Network interface is not ready, required additional network setup");
+    info("Network interface is not ready, required additional network setup");
     return false;
   }
-  safeInfo("Network setup is not required");
+  info("Network setup is not required");
   return true;
 }
 
 /**
  * Network setup utils for Gonk
  *
  * @return {object} providing functions for setup/teardown data connection
  */
@@ -112,10 +112,10 @@ function startNetworkAndTest() {
 function networkTestFinished() {
   var p;
   if ("nsINetworkInterfaceListService" in SpecialPowers.Ci) {
     var utils = getNetworkUtils();
     p = utils.tearDownNetwork();
   } else {
     p = Promise.resolve();
   }
-  return p.then(() => SimpleTest.finish());
+  return p.then(() => finish());
 }
--- a/dom/media/tests/mochitest/pc.js
+++ b/dom/media/tests/mochitest/pc.js
@@ -117,28 +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;
 }
 
 /**
- * A wrapper around runTest() which handles B2G network setup and teardown
- */
-function runNetworkTest(testFunction) {
-  // Use addEventListener to avoid SimpleTest hacking an .onload assignment
-  window.addEventListener('load', () => {
-    SimpleTest.waitForExplicitFinish();
-    startNetworkAndTest()
-      .then(() => realRunTest(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]
@@ -237,17 +225,17 @@ PeerConnectionTest.prototype.closePC = f
     closeIt(this.pcRemote)
   ]), 60000, "failed to close peer connection");
 };
 
 /**
  * Close the open data channels, followed by the underlying peer connection
  */
 PeerConnectionTest.prototype.close = function() {
-  var allChannels = (this.pcLocal ? this.pcLocal : this.pcRemote).dataChannels;
+  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
@@ -312,18 +300,18 @@ PeerConnectionTest.prototype.send = func
   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];
 
   return new Promise(resolve => {
     // Register event handler for the target channel
-    target.onmessage = recv_data => {
-      resolve({ channel: target, data: recv_data });
+    target.onmessage = e => {
+      resolve({ channel: target, data: e.data });
     };
 
     source.send(data);
   });
 };
 
 /**
  * Create a data channel
@@ -616,23 +604,24 @@ function DataChannelWrapper(dataChannel,
   this._channel = dataChannel;
   this._pc = peerConnectionWrapper;
 
   info("Creating " + this);
 
   /**
    * Setup appropriate callbacks
    */
-  guardEvent(this, this._channel, 'close');
-  guardEvent(this, this._channel, 'error');
-  guardEvent(this, this._channel, 'message', e => e.data);
+  createOneShotEventWrapper(this, this._channel, 'close');
+  createOneShotEventWrapper(this, this._channel, 'error');
+  createOneShotEventWrapper(this, this._channel, 'message');
 
   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
@@ -746,19 +735,17 @@ function PeerConnectionWrapper(label, co
 
   this.constraints = [ ];
   this.offerOptions = {};
   this.streams = [ ];
   this.mediaCheckers = [ ];
 
   this.dataChannels = [ ];
 
-  this.onAddStreamAudioCounter = 0;
-  this.onAddStreamVideoCounter = 0;
-  this.addStreamCallbacks = {};
+  this.onAddStreamFired = false;
 
   this._local_ice_candidates = [];
   this._remote_ice_candidates = [];
   this.holdIceCandidates = new Promise(r => this.releaseIceCandidates = r);
   this.localRequiresTrickleIce = false;
   this.remoteRequiresTrickleIce = false;
   this.localMediaElements = [];
 
@@ -798,36 +785,25 @@ function PeerConnectionWrapper(label, co
       type = 'audio';
       self.onAddStreamAudioCounter += event.stream.getAudioTracks().length;
     }
     if (event.stream.getVideoTracks().length > 0) {
       type += 'video';
       self.onAddStreamVideoCounter += event.stream.getVideoTracks().length;
     }
     this.attachMedia(event.stream, type, 'remote');
+  };
 
-    Object.keys(this.addStreamCallbacks).forEach(name => {
-      info(this + " calling addStreamCallback " + name);
-      this.addStreamCallbacks[name]();
-    });
-   };
-
-  guardEvent(this, this._pc, 'datachannel', e => {
+  createOneShotEventWrapper(this, this._pc, 'datachannel');
+  this._pc.addEventListener('datachannel', e => {
     var wrapper = new DataChannelWrapper(e.channel, this);
     this.dataChannels.push(wrapper);
-    return wrapper;
   });
 
-  this.signalingStateCallbacks = {};
-  guardEvent(this, this._pc, 'signalingstatechange', e => {
-    Object.keys(this.signalingStateCallbacks).forEach(name => {
-      this.signalingStateCallbacks[name](e);
-    });
-    return e;
-  });
+  createOneShotEventWrapper(this, this._pc, 'signalingstatechange');
 }
 
 PeerConnectionWrapper.prototype = {
 
   /**
    * Returns the local description.
    *
    * @returns {object} The local description
@@ -956,19 +932,19 @@ PeerConnectionWrapper.prototype = {
   },
 
   /**
    * 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 = channel => {
-        ok(channel, message);
-        resolve(channel);
+      this.ondatachannel = e => {
+        ok(e.channel, message);
+        resolve(e.channel);
       };
     });
   },
 
   /**
    * Create a new data channel instance
    *
    * @param {Object} options
@@ -1077,26 +1053,26 @@ PeerConnectionWrapper.prototype = {
   },
 
   /**
    * Registers a callback for the signaling state change and
    * appends the new state to an array for logging it later.
    */
   logSignalingState: function() {
     this.signalingStateLog = [this._pc.signalingState];
-    this.signalingStateCallbacks.logSignalingStatus = e => {
+    this._pc.addEventListener('signalingstatechange', e => {
       var newstate = this._pc.signalingState;
       var oldstate = this.signalingStateLog[this.signalingStateLog.length - 1]
-      if (Object.keys(signalingStateTransitions).indexOf(oldstate) != -1) {
-        ok(signalingStateTransitions[oldstate].indexOf(newstate) != -1, this + ": legal signaling state transition from " + oldstate + " to " + newstate);
+      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, this + ": old signaling state " + oldstate + " missing in signaling transition array");
       }
       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
@@ -1371,17 +1347,17 @@ PeerConnectionWrapper.prototype = {
 
   /**
    * Checks that we are getting the media tracks we expect.
    *
    * @param {object} constraintsRemote
    *        The media constraints of the local and remote peer connection object
    */
   checkMediaTracks : function(constraintsRemote) {
-    var _checkMediaTracks = constraintsRemote => {
+    var _checkMediaTracks = () => {
       var localConstraintAudioTracks =
         this.countAudioTracksInMediaConstraint(this.constraints);
       var localStreams = this._pc.getLocalStreams();
       var localAudioTracks = this.countAudioTracksInStreams(localStreams, false);
       is(localAudioTracks, localConstraintAudioTracks, this + ' has ' +
         localAudioTracks + ' local audio tracks');
 
       var localConstraintVideoTracks =
@@ -1407,37 +1383,24 @@ PeerConnectionWrapper.prototype = {
     // we have to do this check as the onaddstream never fires if the remote
     // stream has no track at all!
     var expectedRemoteTracks =
       this.countAudioTracksInMediaConstraint(constraintsRemote) +
       this.countVideoTracksInMediaConstraint(constraintsRemote);
 
     // TODO: remove this once Bugs 998552 and 998546 are closed
     if (this.onAddStreamFired || (expectedRemoteTracks == 0)) {
-      _checkMediaTracks(constraintsRemote);
+      _checkMediaTracks();
       return Promise.resolve();
     }
 
     info(this + " checkMediaTracks() got called before onAddStream fired");
-    // we rely on the outer mochitest timeout to catch the case where
-    // onaddstream never fires
-    var happy = new Promise(resolve => {
-      this.addStreamCallbacks.checkMediaTracks = resolve;
-    }).then(() => {
-      _checkMediaTracks(constraintsRemote);
-    });
-    var sad = wait(60000).then(() => {
-      if (!this.onAddStreamFired) {
-        // throw rather than call ok(false) because we only want this to be
-        // caught if the sad path fails the promise race with the happy path
-        throw new Error(this + " checkMediaTracks() timed out waiting" +
-                        " for onaddstream event to fire");
-      }
-    });
-    return Promise.race([ happy, sad ]);
+    var checkPromise = new Promise(r => this._pc.addEventListener('addstream', r))
+      .then(_checkMediaTracks);
+    return timerGuard(checkPromise, 60000, "onaddstream never fired");
   },
 
   checkMsids: function() {
     function _checkMsids(desc, streams, sdpLabel) {
       streams.forEach(stream => {
         stream.getTracks().forEach(track => {
           // TODO(bug 1089798): Once DOMMediaStream has an id field, we
           // should be verifying that the SDP contains
@@ -1784,30 +1747,38 @@ PeerConnectionWrapper.prototype = {
    *
    * @returns {String} The string representation
    */
   toString : function() {
     return "PeerConnectionWrapper (" + this.label + ")";
   }
 };
 
-function createHTML(options) {
-  window.addEventListener('load', () => {
-    realCreateHTML(options);
-  });
-}
-
-[
+var scriptsReady = Promise.all([
   "/tests/SimpleTest/SimpleTest.js",
   "head.js",
   "templates.js",
   "turnConfig.js",
   "dataChannel.js",
   "network.js"
-].forEach(script => {
+].map(script => {
   var el = document.createElement("script");
   if (typeof scriptRelativePath === 'string' && script.charAt(0) !== "/") {
     el.src = scriptRelativePath + script;
   } else {
     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(() => {
+    if (window.SimpleTest) {
+      SimpleTest.waitForExplicitFinish();
+    }
+    return 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
@@ -73,17 +73,17 @@ function waitForIceConnected(test, pc) {
     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: " + test.pcLocal.iceConnectionLog);
+      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 ||
@@ -126,25 +126,19 @@ function checkTrackStats(pc, audio, outb
       isRemote: false
     }), msg + "2");
     ok(!pc.hasStat(stats, {
       mediaType: audio ? "video" : "audio"
     }), msg + "3");
   });
 }
 
-function checkAllTrackStats(pc) {
-  var checks = [];
-  for (var i = 0; i < 4; ++i) {
-    // check all combinations
-    checks.push(checkTrackStats(pc, (i & 1) === 1, (i & 2) === 2));
-  }
-  return Promise.all(checks);
-}
-
+// 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);
@@ -349,17 +343,17 @@ var commandsPeerConnection = [
 
   function PC_LOCAL_GET_ANSWER(test) {
     if (!test.steeplechase) {
       test._remote_answer = test.originalAnswer;
       test._answer_constraints = test.pcRemote.constraints;
       return Promise.resolve();
     }
 
-    test.getSignalingMessage("answer").then(message => {
+    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)
@@ -435,17 +429,17 @@ var commandsPeerConnection = [
     });
   },
 
   function PC_LOCAL_CHECK_ICE_CONNECTIONS(test) {
     test.pcLocal.getStats(null).then(stats => {
       test.pcLocal.checkStatsIceConnections(stats,
                                             test._offer_constraints,
                                             test._offer_options,
-                                            test.originalAnswer);
+                                            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,
@@ -459,11 +453,11 @@ var commandsPeerConnection = [
   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.pcLocal);
+    return checkAllTrackStats(test.pcRemote);
   }
 ];