Bug 1231784 - Install specialpowers and mochikit extensions at runtime via AddonManager.loadTemporaryAddon(), r=jgriffin draft
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Mon, 25 Jan 2016 09:55:57 -0500
changeset 327078 679b93ddcd54cd8f1bddfa8dc96edb7522fc3281
parent 327069 8a3c5c9b1486c3328bc4684d5ac5b3b849b09474
child 327079 3f0fb69072bbf6d03bdcc953ac10cc186596b75f
push id10187
push userahalberstadt@mozilla.com
push dateFri, 29 Jan 2016 15:02:07 +0000
reviewersjgriffin
bugs1231784
milestone47.0a1
Bug 1231784 - Install specialpowers and mochikit extensions at runtime via AddonManager.loadTemporaryAddon(), r=jgriffin
build/mobile/remoteautomation.py
testing/marionette/driver/marionette_driver/marionette.py
testing/mochitest/b2g_start_script.js
testing/mochitest/bootstrap.js
testing/mochitest/browser-harness.xul
testing/mochitest/browser-test-overlay.xul
testing/mochitest/browser-test.js
testing/mochitest/install.rdf
testing/mochitest/jetpack-addon-harness.js
testing/mochitest/jetpack-package-harness.js
testing/mochitest/mochitest_options.py
testing/mochitest/moz.build
testing/mochitest/runtests.py
testing/mochitest/runtestsb2g.py
testing/mochitest/runtestsremote.py
testing/mochitest/start_b2g.js
testing/mochitest/start_desktop.js
testing/profiles/prefs_general.js
--- a/build/mobile/remoteautomation.py
+++ b/build/mobile/remoteautomation.py
@@ -2,16 +2,17 @@
 # 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/.
 
 import datetime
 import glob
 import time
 import re
 import os
+import posixpath
 import tempfile
 import shutil
 import subprocess
 import sys
 
 from automation import Automation
 from devicemanager import DMError, DeviceManager
 from mozlog import get_default_logger
@@ -209,17 +210,17 @@ class RemoteAutomation(Automation):
 
         # If crash reporting is disabled (MOZ_CRASHREPORTER!=1), we can't say
         # anything.
         if not self.CRASHREPORTER:
             return False
 
         try:
             dumpDir = tempfile.mkdtemp()
-            remoteCrashDir = self._remoteProfile + '/minidumps/'
+            remoteCrashDir = posixpath.join(self._remoteProfile, 'minidumps')
             if not self._devicemanager.dirExists(remoteCrashDir):
                 # If crash reporting is enabled (MOZ_CRASHREPORTER=1), the
                 # minidumps directory is automatically created when Fennec
                 # (first) starts, so its lack of presence is a hint that
                 # something went wrong.
                 print "Automation Error: No crash directory (%s) found on remote device" % remoteCrashDir
                 # Whilst no crash was found, the run should still display as a failure
                 return True
--- a/testing/marionette/driver/marionette_driver/marionette.py
+++ b/testing/marionette/driver/marionette_driver/marionette.py
@@ -1144,18 +1144,21 @@ class Marionette(object):
             if returncode is not None:
                 # We're managing a binary which has terminated, so restart it.
                 self.instance.restart()
 
         self.client = transport.TcpTransport(
             self.host,
             self.port,
             self.socket_timeout)
+
+        # Call wait_for_port() before attempting to connect in
+        # the event gecko hasn't started yet.
+        self.wait_for_port(timeout=timeout)
         self.protocol, _ = self.client.connect()
-        self.wait_for_port(timeout=timeout)
 
         body = {"capabilities": desired_capabilities, "sessionId": session_id}
         resp = self._send_message("newSession", body)
 
         self.session_id = resp["sessionId"]
         self.session = resp["value"] if self.protocol == 1 else resp["capabilities"]
         self.b2g = "b2g" in self.session
 
new file mode 100644
--- /dev/null
+++ b/testing/mochitest/bootstrap.js
@@ -0,0 +1,84 @@
+/* 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/. */
+
+const { utils: Cu, interfaces: Ci, classes: Cc } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+var WindowListener = {
+  // browser-test.js is only loaded into the first window. Setup that
+  // needs to happen in all navigator:browser windows should go here.
+  setupWindow: function(win) {
+    win.nativeConsole = win.console;
+    XPCOMUtils.defineLazyModuleGetter(win, "console",
+      "resource://gre/modules/Console.jsm");
+  },
+
+  tearDownWindow: function(win) {
+    if (win.nativeConsole) {
+      win.console = win.nativeConsole;
+      win.nativeConsole = undefined;
+    }
+  },
+
+  onOpenWindow: function (win) {
+    win = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
+
+    win.addEventListener("load", function listener() {
+      win.removeEventListener("load", listener, false);
+      if (win.document.documentElement.getAttribute("windowtype") == "navigator:browser") {
+        WindowListener.setupWindow(win);
+      }
+    }, false);
+  }
+}
+
+function loadMochitest(e) {
+  let flavor = e.detail[0];
+  let url = e.detail[1];
+
+  let win = Services.wm.getMostRecentWindow("navigator:browser");
+  win.removeEventListener('mochitest-load', loadMochitest);
+
+  // for mochitest-plain, navigating to the url is all we need
+  win.loadURI(url);
+  if (flavor == "mochitest") {
+    return;
+  }
+
+  WindowListener.setupWindow(win);
+  Services.wm.addListener(WindowListener);
+
+  let overlay;
+  if (flavor == "jetpack-addon") {
+    overlay = "chrome://mochikit/content/jetpack-addon-overlay.xul";
+  } else if (flavor == "jetpack-package") {
+    overlay = "chrome://mochikit/content/jetpack-package-overlay.xul";
+  } else {
+    overlay = "chrome://mochikit/content/browser-test-overlay.xul";
+  }
+
+  win.document.loadOverlay(overlay, null);
+}
+
+function startup(data, reason) {
+  let win = Services.wm.getMostRecentWindow("navigator:browser");
+  // wait for event fired from start_desktop.js containing the
+  // suite and url to load
+  win.addEventListener('mochitest-load', loadMochitest);
+}
+
+function shutdown(data, reason) {
+  let windows = Services.wm.getEnumerator("navigator:browser");
+  while (windows.hasMoreElements()) {
+    let win = windows.getNext().QueryInterface(Ci.nsIDOMWindow);
+    WindowListener.tearDownWindow(win);
+  }
+
+  Services.wm.removeListener(WindowListener);
+}
+
+function install(data, reason) {}
+function uninstall(data, reason) {}
--- a/testing/mochitest/browser-harness.xul
+++ b/testing/mochitest/browser-harness.xul
@@ -246,17 +246,17 @@
       // It's possible that the test harness window is not yet focused when this
       // function runs (in which case testWin is already focused, and focusing it
       // will be a no-op, and then the test harness window will steal focus later,
       // which will mess up tests). So wait for the test harness window to be
       // focused before trying to focus testWin.
       waitForFocus(() => {
         // Focus the test window and start tests.
         waitForFocus(() => {
-          var Tester = new testWin.Tester(links, gDumper, testsFinished);
+          var Tester = new testWin.Tester(links, gDumper.structuredLogger, testsFinished);
           Tester.start();
         }, testWin);
       }, window);
     }
 
     function executeSoon(callback) {
       let tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager);
       tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
--- a/testing/mochitest/browser-test-overlay.xul
+++ b/testing/mochitest/browser-test-overlay.xul
@@ -2,12 +2,12 @@
 <?xml-stylesheet href="chrome://global/skin" type="text/css"?>
 <!-- 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/. -->
 
 <overlay id="browserTestOverlay"
          xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
   <script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"/>
+  <script type="application/javascript" src="chrome://mochikit/content/mochitest-e10s-utils.js"/>
   <script type="application/javascript" src="chrome://mochikit/content/browser-test.js"/>
   <script type="application/javascript" src="chrome://mochikit/content/cc-analyzer.js"/>
-  <script type="application/javascript" src="chrome://mochikit/content/mochitest-e10s-utils.js"/>
 </overlay>
--- a/testing/mochitest/browser-test.js
+++ b/testing/mochitest/browser-test.js
@@ -1,53 +1,42 @@
 /* -*- js-indent-level: 2; tab-width: 2; indent-tabs-mode: nil -*- */
 // Test timeout (seconds)
 var gTimeoutSeconds = 45;
 var gConfig;
 
-if (Cc === undefined) {
-  var Cc = Components.classes;
-}
-if (Ci === undefined) {
-  var Ci = Components.interfaces;
-}
-if (Cu === undefined) {
-  var Cu = Components.utils;
-}
-
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/AppConstants.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "Services",
-  "resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizationTabPreloader",
   "resource:///modules/CustomizationTabPreloader.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ContentSearch",
   "resource:///modules/ContentSearch.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "SelfSupportBackend",
   "resource:///modules/SelfSupportBackend.jsm");
 
-var nativeConsole = console;
-XPCOMUtils.defineLazyModuleGetter(this, "console",
-  "resource://gre/modules/Console.jsm");
-
 const SIMPLETEST_OVERRIDES =
   ["ok", "is", "isnot", "todo", "todo_is", "todo_isnot", "info", "expectAssertions", "requestCompleteLog"];
 
-window.addEventListener("load", function testOnLoad() {
-  window.removeEventListener("load", testOnLoad);
-  window.addEventListener("MozAfterPaint", function testOnMozAfterPaint() {
-    window.removeEventListener("MozAfterPaint", testOnMozAfterPaint);
-    setTimeout(testInit, 0);
+// non-android is bootstrapped by marionette
+if (Services.appinfo.OS == 'Android') {
+  window.addEventListener("load", function testOnLoad() {
+    window.removeEventListener("load", testOnLoad);
+    window.addEventListener("MozAfterPaint", function testOnMozAfterPaint() {
+      window.removeEventListener("MozAfterPaint", testOnMozAfterPaint);
+      setTimeout(testInit, 0);
+    });
   });
-});
+} else {
+  setTimeout(testInit, 0);
+}
 
 function b2gStart() {
   let homescreen = document.getElementById('systemapp');
   var webNav = homescreen.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                                    .getInterface(Ci.nsIWebNavigation);
   var url = "chrome://mochikit/content/harness.xul?manifestFile=tests.json";
 
   webNav.loadURI(url, null, null, null, null);
@@ -135,18 +124,18 @@ function testInit() {
     // In non-e10s, only run the ShutdownLeaksCollector in the parent process.
     Components.utils.import("chrome://mochikit/content/ShutdownLeaksCollector.jsm");
   }
 
   let gmm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
   gmm.loadFrameScript("chrome://mochikit/content/tests/SimpleTest/AsyncUtilsContent.js", true);
 }
 
-function Tester(aTests, aDumper, aCallback) {
-  this.dumper = aDumper;
+function Tester(aTests, structuredLogger, aCallback) {
+  this.structuredLogger = structuredLogger;
   this.tests = aTests;
   this.callback = aCallback;
   this.openedWindows = {};
   this.openedURLs = {};
 
   this._scriptLoader = Services.scriptloader;
   this.EventUtils = {};
   this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", this.EventUtils);
@@ -252,17 +241,17 @@ Tester.prototype = {
 
     if (gConfig.jscovDirPrefix) {
       let coveragePath = gConfig.jscovDirPrefix;
       let {CoverageCollector} = Cu.import("resource://testing-common/CoverageUtils.jsm",
                                           {});
       this._coverageCollector = new CoverageCollector(coveragePath);
     }
 
-    this.dumper.structuredLogger.info("*** Start BrowserChrome Test Results ***");
+    this.structuredLogger.info("*** Start BrowserChrome Test Results ***");
     Services.console.registerListener(this);
     Services.obs.addObserver(this, "chrome-document-global-created", false);
     Services.obs.addObserver(this, "content-document-global-created", false);
     this._globalProperties = Object.keys(window);
     this._globalPropertyWhitelist = [
       "navigator", "constructor", "top",
       "Application",
       "__SS_tabsToRestore", "__SSi",
@@ -313,17 +302,17 @@ Tester.prototype = {
     // Replace the last tab with a fresh one
     if (window.gBrowser) {
       gBrowser.addTab("about:blank", { skipAnimation: true });
       gBrowser.removeTab(gBrowser.selectedTab, { skipPermitUnload: true });
       gBrowser.stop();
     }
 
     // Remove stale windows
-    this.dumper.structuredLogger.info("checking window state");
+    this.structuredLogger.info("checking window state");
     let windowsEnum = Services.wm.getEnumerator(null);
     let createdFakeTestForLogging = false;
     while (windowsEnum.hasMoreElements()) {
       let win = windowsEnum.getNext();
       if (win != window && !win.closed &&
           win.document.documentElement.getAttribute("id") != "browserTestHarness") {
         let type = win.document.documentElement.getAttribute("windowtype");
         switch (type) {
@@ -336,32 +325,32 @@ Tester.prototype = {
           break;
         }
         let msg = baseMsg.replace("{elt}", type);
         if (this.currentTest) {
           this.currentTest.addResult(new testResult(false, msg, "", false));
         } else {
           if (!createdFakeTestForLogging) {
             createdFakeTestForLogging = true;
-            this.dumper.structuredLogger.testStart("browser-test.js");
+            this.structuredLogger.testStart("browser-test.js");
           }
           this.failuresFromInitialWindowState++;
-          this.dumper.structuredLogger.testStatus("browser-test.js",
-                                                  msg, "FAIL", false, "");
+          this.structuredLogger.testStatus("browser-test.js",
+                                           msg, "FAIL", false, "");
         }
 
         win.close();
       }
     }
     if (createdFakeTestForLogging) {
       let time = Date.now() - startTime;
-      this.dumper.structuredLogger.testEnd("browser-test.js",
-                                           "OK",
-                                           undefined,
-                                           "finished window state check in " + time + "ms");
+      this.structuredLogger.testEnd("browser-test.js",
+                                    "OK",
+                                    undefined,
+                                    "finished window state check in " + time + "ms");
     }
 
     // Make sure the window is raised before each test.
     this.SimpleTest.waitForFocus(aCallback);
   },
 
   finish: function Tester_finish(aSkipSummary) {
     this.Promise.Debugging.flushUncaughtErrors();
@@ -385,32 +374,30 @@ Tester.prototype = {
       Services.obs.removeObserver(this, "content-document-global-created");
       this.Promise.Debugging.clearUncaughtErrorObservers();
       this._treatUncaughtRejectionsAsFailures = false;
 
       // In the main process, we print the ShutdownLeaksCollector message here.
       let pid = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).processID;
       dump("Completed ShutdownLeaks collections in process " + pid + "\n");
 
-      this.dumper.structuredLogger.info("TEST-START | Shutdown");
+      this.structuredLogger.info("TEST-START | Shutdown");
 
       if (this.tests.length) {
-        this.dumper.structuredLogger.info("Browser Chrome Test Summary");
-        this.dumper.structuredLogger.info("Passed:  " + passCount);
-        this.dumper.structuredLogger.info("Failed:  " + failCount);
-        this.dumper.structuredLogger.info("Todo:    " + todoCount);
+        this.structuredLogger.info("Browser Chrome Test Summary");
+        this.structuredLogger.info("Passed:  " + passCount);
+        this.structuredLogger.info("Failed:  " + failCount);
+        this.structuredLogger.info("Todo:    " + todoCount);
       } else {
-        this.dumper.structuredLogger.testEnd("browser-test.js",
-                                             "FAIL",
-                                             "PASS",
-                                             "No tests to run. Did you pass invalid test_paths?");
+        this.structuredLogger.testEnd("browser-test.js",
+                                      "FAIL",
+                                      "PASS",
+                                      "No tests to run. Did you pass invalid test_paths?");
       }
-      this.dumper.structuredLogger.info("*** End BrowserChrome Test Results ***");
-
-      this.dumper.done();
+      this.structuredLogger.info("*** End BrowserChrome Test Results ***");
 
       // Tests complete, notify the callback and return
       this.callback(this.tests);
       this.callback = null;
       this.tests = null;
       this.openedWindows = null;
     }
   },
@@ -449,17 +436,17 @@ Tester.prototype = {
     if (!aConsoleMessage.message)
       return;
 
     try {
       var msg = "Console message: " + aConsoleMessage.message;
       if (this.currentTest)
         this.currentTest.addResult(new testMessage(msg));
       else
-        this.dumper.dump("TEST-INFO | (browser-test.js) | " + msg.replace(/\n$/, "") + "\n");
+        this.structuredLogger.info("TEST-INFO | (browser-test.js) | " + msg.replace(/\n$/, "") + "\n");
     } catch (ex) {
       // Swallow exception so we don't lead to another error being reported,
       // throwing us into an infinite loop
     }
   },
 
   nextTest: Task.async(function*() {
     if (this.currentTest) {
@@ -576,17 +563,17 @@ Tester.prototype = {
                               this.currentTest.path,
                               gConfig.dumpOutputDirectory,
                               gConfig.dumpAboutMemoryAfterTest,
                               gConfig.dumpDMDAfterTest);
       }
 
       // Note the test run time
       let time = Date.now() - this.lastStartTime;
-      this.dumper.structuredLogger.testEnd(this.currentTest.path,
+      this.structuredLogger.testEnd(this.currentTest.path,
                                            "OK",
                                            undefined,
                                            "finished in " + time + "ms");
       this.currentTest.setDuration(time);
 
       if (this.runUntilFailure && this.currentTest.failCount > 0) {
         this.haltTests();
       }
@@ -720,17 +707,17 @@ Tester.prototype = {
       }
 
       this.currentTestIndex++;
       this.execTest();
     }).bind(this));
   }),
 
   execTest: function Tester_execTest() {
-    this.dumper.structuredLogger.testStart(this.currentTest.path);
+    this.structuredLogger.testStart(this.currentTest.path);
 
     this.SimpleTest.reset();
 
     // Load the tests into a testscope
     let currentScope = this.currentTest.scope = new testScope(this, this.currentTest, this.currentTest.expected);
     let currentTest = this.currentTest;
 
     // Import utils in the test scope.
@@ -1073,19 +1060,19 @@ function testScope(aTester, aTest, expec
           self.__waitTimer = null;
           self.__tester.nextTest();
         }
       });
     }
   };
 
   this.requestCompleteLog = function test_requestCompleteLog() {
-    self.__tester.dumper.structuredLogger.deactivateBuffering();
+    self.__tester.structuredLogger.deactivateBuffering();
     self.registerCleanupFunction(function() {
-      self.__tester.dumper.structuredLogger.activateBuffering();
+      self.__tester.structuredLogger.activateBuffering();
     })
   };
 }
 testScope.prototype = {
   __done: true,
   __tasks: null,
   __waitTimer: null,
   __cleanupFunctions: [],
--- a/testing/mochitest/install.rdf
+++ b/testing/mochitest/install.rdf
@@ -1,15 +1,18 @@
 <?xml version="1.0"?>
 
 <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
      xmlns:em="http://www.mozilla.org/2004/em-rdf#">
   <Description about="urn:mozilla:install-manifest">
     <em:id>mochikit@mozilla.org</em:id>
     <em:version>1.0</em:version>
+#ifdef MOCHITEST_BOOTSTRAP
+    <em:bootstrap>true</em:bootstrap>
+#endif
     <em:targetApplication>
       <Description>
         <em:id>toolkit@mozilla.org</em:id>
 #expand        <em:minVersion>__MOZILLA_VERSION_U__</em:minVersion>
 #expand        <em:maxVersion>__MOZILLA_VERSION_U__</em:maxVersion>
       </Description>
     </em:targetApplication>
     <!-- Front End MetaData -->
--- a/testing/mochitest/jetpack-addon-harness.js
+++ b/testing/mochitest/jetpack-addon-harness.js
@@ -11,24 +11,17 @@ Cu.import("resource://gre/modules/XPCOMU
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
   "resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
   "resource://gre/modules/AddonManager.jsm");
 
-// Start the tests after the window has been displayed
-window.addEventListener("load", function testOnLoad() {
-  window.removeEventListener("load", testOnLoad);
-  window.addEventListener("MozAfterPaint", function testOnMozAfterPaint() {
-    window.removeEventListener("MozAfterPaint", testOnMozAfterPaint);
-    setTimeout(testInit, 0);
-  });
-});
+setTimeout(testInit, 0);
 
 var sdkpath = null;
 
 // Strip off the chrome prefix to get the actual path of the test directory
 function realPath(chrome) {
   return chrome.substring("chrome://mochitests/content/jetpack-addon/".length)
                .replace(".xpi", "");
 }
--- a/testing/mochitest/jetpack-package-harness.js
+++ b/testing/mochitest/jetpack-package-harness.js
@@ -13,24 +13,17 @@ if (Cc === undefined) {
 }
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
   "resource://gre/modules/Services.jsm");
 
-// Start the tests after the window has been displayed
-window.addEventListener("load", function testOnLoad() {
-  window.removeEventListener("load", testOnLoad);
-  window.addEventListener("MozAfterPaint", function testOnMozAfterPaint() {
-    window.removeEventListener("MozAfterPaint", testOnMozAfterPaint);
-    setTimeout(testInit, 0);
-  });
-});
+setTimeout(testInit, 0);
 
 // Tests a single module
 function testModule(require, { url, expected }) {
   return new Promise(resolve => {
     let path = url.substring(TEST_PACKAGE.length);
 
     const { stdout } = require("sdk/system");
 
--- a/testing/mochitest/mochitest_options.py
+++ b/testing/mochitest/mochitest_options.py
@@ -540,23 +540,27 @@ class MochitestArguments(ArgumentContain
                   "times in which case the test must contain at least one of the given tags.",
           }],
         [["--enable-cpow-warnings"],
          {"action": "store_true",
           "dest": "enableCPOWWarnings",
           "help": "Enable logging of unsafe CPOW usage, which is disabled by default for tests",
           "suppress": True,
           }],
+        [["--marionette"],
+         {"default": None,
+          "help": "host:port to use when connecting to Marionette",
+          }],
     ]
 
     defaults = {
         # Bug 1065098 - The geckomediaplugin process fails to produce a leak
         # log for some reason.
         'ignoreMissingLeaks': ["geckomediaplugin"],
-
+        'extensionsToExclude': ['specialpowers'],
         # Set server information on the args object
         'webServer': '127.0.0.1',
         'httpPort': DEFAULT_PORTS['http'],
         'sslPort': DEFAULT_PORTS['https'],
         'webSocketPort': '9988',
         # The default websocket port is incorrect in mozprofile; it is
         # set to the SSL proxy setting. See:
         # see https://bugzilla.mozilla.org/show_bug.cgi?id=916517
@@ -804,20 +808,16 @@ class B2GArguments(ArgumentContainer):
 
     args = [
         [["--b2gpath"],
          {"dest": "b2gPath",
           "default": None,
           "help": "Path to B2G repo or QEMU directory.",
           "suppress": True,
           }],
-        [["--marionette"],
-         {"default": None,
-          "help": "host:port to use when connecting to Marionette",
-          }],
         [["--emulator"],
          {"default": None,
           "help": "Architecture of emulator to use, x86 or arm",
           "suppress": True,
           }],
         [["--wifi"],
          {"default": False,
           "help": "Devine wifi configuration for on device mochitest",
@@ -901,16 +901,18 @@ class B2GArguments(ArgumentContainer):
           }],
     ]
 
     defaults = {
         'logFile': 'mochitest.log',
         # Specialpowers is integrated with marionette for b2g,
         # see marionette's jar.mn.
         'extensionsToExclude': ['specialpowers'],
+        # mochijar doesn't get installed via marionette on android
+        'extensionsToInstall': [os.path.join(here, 'mochijar')],
         # See dependencies of bug 1038943.
         'defaultLeakThreshold': 5536,
     }
 
     def validate(self, parser, options, context):
         """Validate b2g options."""
 
         if options.remoteWebServer is None:
@@ -1053,16 +1055,20 @@ class AndroidArguments(ArgumentContainer
           "help": "remote directory to use as test root \
                    (eg. /mnt/sdcard/tests or /data/local/tests)",
           "suppress": True,
           }],
     ]
 
     defaults = {
         'dm': None,
+        # we don't want to exclude specialpowers on android just yet
+        'extensionsToExclude': [],
+        # mochijar doesn't get installed via marionette on android
+        'extensionsToInstall': [os.path.join(here, 'mochijar')],
         'logFile': 'mochitest.log',
         'utilityPath': None,
     }
 
     def validate(self, parser, options, context):
         """Validate android options."""
 
         if build_obj:
--- a/testing/mochitest/moz.build
+++ b/testing/mochitest/moz.build
@@ -14,16 +14,20 @@ DIRS += [
 XPI_NAME = 'mochijar'
 
 JAR_MANIFESTS += ['jar.mn']
 
 USE_EXTENSION_MANIFEST = True
 
 FINAL_TARGET_PP_FILES += ['install.rdf']
 
+if CONFIG['MOZ_BUILD_APP'] != 'mobile/android' and CONFIG['MOZ_WIDGET_TOOLKIT'] != 'gonk':
+    DEFINES['MOCHITEST_BOOTSTRAP'] = True
+    FINAL_TARGET_FILES += ['bootstrap.js']
+
 MOCHITEST_MANIFESTS += [
     'tests/MochiKit-1.4.2/tests/mochitest.ini',
 ]
 MOCHITEST_CHROME_MANIFESTS += ['chrome/chrome.ini']
 
 TEST_HARNESS_FILES.testing.mochitest += [
     '!automation.py',
     '/build/mobile/remoteautomation.py',
@@ -32,17 +36,16 @@ TEST_HARNESS_FILES.testing.mochitest += 
     '/netwerk/test/httpserver/httpd.js',
     '/testing/mozbase/mozdevice/mozdevice/devicemanager.py',
     '/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py',
     '/testing/mozbase/mozdevice/mozdevice/devicemanagerSUT.py',
     '/testing/mozbase/mozdevice/mozdevice/droid.py',
     '/testing/mozbase/mozdevice/mozdevice/version_codes.py',
     '/testing/mozbase/mozdevice/mozdevice/Zeroconf.py',
     '/testing/mozbase/moznetwork/moznetwork/moznetwork.py',
-    'b2g_start_script.js',
     'bisection.py',
     'browser-harness.xul',
     'browser-test-overlay.xul',
     'browser-test.js',
     'cc-analyzer.js',
     'chrome-harness.js',
     'chunkifyTests.js',
     'gen_template.pl',
@@ -59,16 +62,18 @@ TEST_HARNESS_FILES.testing.mochitest += 
     'nested_setup.js',
     'pywebsocket_wrapper.py',
     'redirect.html',
     'runrobocop.py',
     'runtests.py',
     'runtestsb2g.py',
     'runtestsremote.py',
     'server.js',
+    'start_b2g.js',
+    'start_desktop.js',
 ]
 
 TEST_HARNESS_FILES.testing.mochitest.pywebsocket += [
     'pywebsocket/standalone.py',
 ]
 
 TEST_HARNESS_FILES.testing.mochitest.pywebsocket.mod_pywebsocket += [
     'pywebsocket/mod_pywebsocket/__init__.py',
--- a/testing/mochitest/runtests.py
+++ b/testing/mochitest/runtests.py
@@ -39,16 +39,25 @@ from manifestparser import TestManifest
 from manifestparser.filters import (
     chunk_by_dir,
     chunk_by_runtime,
     chunk_by_slice,
     pathprefix,
     subsuite,
     tags,
 )
+
+try:
+    from marionette import Marionette
+    from marionette_driver.addons import Addons
+
+except ImportError:
+    # Marionette not needed nor supported on android
+    Marionette = None
+
 from leaks import ShutdownLeaks, LSANLeaks
 from mochitest_options import (
     MochitestArgumentParser, build_obj, get_default_valgrind_suppression_files
 )
 from mozprofile import Profile, Preferences
 from mozprofile.permissions import ServerLocations
 from urllib import quote_plus as encodeURIComponent
 from mozlog.formatters import TbplFormatter
@@ -493,46 +502,51 @@ class WebSocketServer(object):
 
 
 class MochitestBase(object):
     """
     Base mochitest class for both desktop and b2g.
     """
 
     oldcwd = os.getcwd()
-    jarDir = 'mochijar'
+    mochijar = os.path.join(SCRIPT_DIR, 'mochijar')
 
     # Path to the test script on the server
     TEST_PATH = "tests"
     NESTED_OOP_TEST_PATH = "nested_oop"
     CHROME_PATH = "redirect.html"
     urlOpts = []
     log = None
 
     def __init__(self, logger_options):
         self.update_mozinfo()
         self.server = None
         self.wsserver = None
         self.sslTunnel = None
         self._active_tests = None
         self._locations = None
 
+        self.marionette = None
+        self.start_script = None
+        self.start_script_args = []
+
         if self.log is None:
             commandline.log_formatters["tbpl"] = (
                 MochitestFormatter,
                 "Mochitest specific tbpl formatter")
             self.log = commandline.setup_logging("mochitest",
                                                  logger_options,
                                                  {
                                                      "tbpl": sys.stdout
                                                  })
             MochitestBase.log = self.log
 
         self.message_logger = MessageLogger(logger=self.log)
 
+
     def update_mozinfo(self):
         """walk up directories to find mozinfo.json update the info"""
         # TODO: This should go in a more generic place, e.g. mozinfo
 
         path = SCRIPT_DIR
         dirs = set()
         while path != os.path.expanduser('~'):
             if path in dirs:
@@ -866,24 +880,16 @@ class MochitestBase(object):
                     options.profilePath,
                     os.path.basename(abspath))
                 shutil.copytree(abspath, dest)
             else:
                 self.log.warning(
                     "runtests.py | Failed to copy %s to profile" %
                     abspath)
 
-    def installChromeJar(self, chrome, options):
-        """
-          copy mochijar directory to profile as an extension so we have chrome://mochikit for all harness code
-        """
-        # Write chrome.manifest.
-        with open(os.path.join(options.profilePath, "extensions", "staged", "mochikit@mozilla.org", "chrome.manifest"), "a") as mfile:
-            mfile.write(chrome)
-
     def getChromeTestDir(self, options):
         dir = os.path.join(os.path.abspath("."), SCRIPT_DIR) + "/"
         if mozinfo.isWin:
             dir = "file:///" + dir.replace("\\", "/")
         return dir
 
     def writeChromeManifest(self, options):
         manifest = os.path.join(options.profilePath, "tests.manifest")
@@ -925,49 +931,26 @@ toolbar#nav-bar {
   background-image: none !important;
 }
 """
         with open(os.path.join(options.profilePath, "userChrome.css"), "a") as chromeFile:
             chromeFile.write(chrome)
 
         manifest = self.writeChromeManifest(options)
 
-        # Call installChromeJar().
-        if not os.path.isdir(os.path.join(SCRIPT_DIR, self.jarDir)):
+        if not os.path.isdir(self.mochijar):
             self.log.info(
                 "TEST-UNEXPECTED-FAIL | invalid setup: missing mochikit extension")
             return None
 
-        # Support Firefox (browser), B2G (shell), SeaMonkey (navigator), and Webapp
-        # Runtime (webapp).
-        chrome = ""
-        if options.browserChrome or options.chrome or options.a11y or options.webapprtChrome:
-            chrome += """
-overlay chrome://browser/content/browser.xul chrome://mochikit/content/browser-test-overlay.xul
-overlay chrome://browser/content/shell.xhtml chrome://mochikit/content/browser-test-overlay.xul
-overlay chrome://navigator/content/navigator.xul chrome://mochikit/content/browser-test-overlay.xul
-overlay chrome://webapprt/content/webapp.xul chrome://mochikit/content/browser-test-overlay.xul
-"""
-
-        if options.jetpackPackage:
-            chrome += """
-overlay chrome://browser/content/browser.xul chrome://mochikit/content/jetpack-package-overlay.xul
-"""
-
-        if options.jetpackAddon:
-            chrome += """
-overlay chrome://browser/content/browser.xul chrome://mochikit/content/jetpack-addon-overlay.xul
-"""
-
-        self.installChromeJar(chrome, options)
         return manifest
 
     def getExtensionsToInstall(self, options):
         "Return a list of extensions to install in the profile"
-        extensions = options.extensionsToInstall or []
+        extensions = []
         appDir = options.app[
             :options.app.rfind(
                 os.sep)] if options.app else options.utilityPath
 
         extensionDirs = [
             # Extensions distributed with the test harness.
             os.path.normpath(os.path.join(SCRIPT_DIR, "extensions")),
         ]
@@ -982,19 +965,17 @@ overlay chrome://browser/content/browser
         for extensionDir in extensionDirs:
             if os.path.isdir(extensionDir):
                 for dirEntry in os.listdir(extensionDir):
                     if dirEntry not in options.extensionsToExclude:
                         path = os.path.join(extensionDir, dirEntry)
                         if os.path.isdir(path) or (
                                 os.path.isfile(path) and path.endswith(".xpi")):
                             extensions.append(path)
-
-        # append mochikit
-        extensions.append(os.path.join(SCRIPT_DIR, self.jarDir))
+        extensions.extend(options.extensionsToInstall)
         return extensions
 
     def logPreamble(self, tests):
         """Logs a suite_start message and test_start/test_end at the beginning of a run.
         """
         self.log.suite_start([t['path'] for t in tests])
         for test in tests:
             if 'disabled' in test:
@@ -1256,16 +1237,30 @@ overlay chrome://browser/content/browser
                 if parts[2] == pname and parts[1] == '1':
                     self.log.info("killing %s orphan with pid %d" % (pname, pid))
                     killPid(pid, self.log)
         process = mozprocess.ProcessHandler(['ps', '-o', 'pid,ppid,comm'],
                                             processOutputLine=_psKill)
         process.run()
         process.wait()
 
+    def execute_start_script(self):
+        if not self.start_script or not self.marionette:
+            return
+
+        if os.path.isfile(self.start_script):
+            with open(self.start_script, 'r') as fh:
+                script = fh.read()
+        else:
+            script = self.start_script
+
+        with self.marionette.using_context('chrome'):
+            return self.marionette.execute_script(script,
+                script_args=self.start_script_args)
+
 
 class SSLTunnel:
 
     def __init__(self, options, logger, ignoreSSLTunnelExts=False):
         self.log = logger
         self.process = None
         self.utilityPath = options.utilityPath
         self.xrePath = options.xrePath
@@ -1500,18 +1495,18 @@ class MochitestDesktop(MochitestBase):
     sslTunnel = None
     DEFAULT_TIMEOUT = 60.0
     mediaDevices = None
 
     # XXX use automation.py for test name to avoid breaking legacy
     # TODO: replace this with 'runtests.py' or 'mochitest' or the like
     test_name = 'automation.py'
 
-    def __init__(self, logger_options):
-        MochitestBase.__init__(self, logger_options)
+    def __init__(self, *args, **kwargs):
+        MochitestBase.__init__(self, *args, **kwargs)
 
         # Max time in seconds to wait for server startup before tests will fail -- if
         # this seems big, it's mostly for debug machines where cold startup
         # (particularly after a build) takes forever.
         self.SERVER_STARTUP_TIMEOUT = 180 if mozinfo.info.get('debug') else 90
 
         # metro browser sub process id
         self.browserProcessId = None
@@ -1520,16 +1515,18 @@ class MochitestDesktop(MochitestBase):
         # Create variables to count the number of passes, fails, todos.
         self.countpass = 0
         self.countfail = 0
         self.counttodo = 0
 
         self.expectedError = {}
         self.result = {}
 
+        self.start_script = os.path.join(here, 'start_desktop.js')
+
     def extraPrefs(self, extraPrefs):
         """interpolate extra preferences from option strings"""
 
         try:
             return dict(parseKeyValue(extraPrefs, context='--setpref='))
         except KeyValueParseError as e:
             print str(e)
             sys.exit(1)
@@ -1834,21 +1831,21 @@ class MochitestDesktop(MochitestBase):
                extraArgs,
                utilityPath,
                debuggerInfo=None,
                valgrindPath=None,
                valgrindArgs=None,
                valgrindSuppFiles=None,
                symbolsPath=None,
                timeout=-1,
-               onLaunch=None,
                detectShutdownLeaks=False,
                screenshotOnFail=False,
                bisectChunk=None,
-               quiet=False):
+               quiet=False,
+               marionette_args=None):
         """
         Run the app, log the duration it took to execute, return the status code.
         Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing for |timeout| seconds.
         """
 
         # configure the message logger buffering
         self.message_logger.buffering = quiet
 
@@ -1902,23 +1899,26 @@ class MochitestDesktop(MochitestBase):
                 # If a debugger is attached, don't use timeouts, and don't
                 # capture ctrl-c.
                 timeout = None
                 signal.signal(signal.SIGINT, lambda sigid, frame: None)
 
             # build command line
             cmd = os.path.abspath(app)
             args = list(extraArgs)
+            args.append('-marionette')
             # TODO: mozrunner should use -foreground at least for mac
             # https://bugzilla.mozilla.org/show_bug.cgi?id=916512
             args.append('-foreground')
             if testUrl:
                 if debuggerInfo and debuggerInfo.requiresEscapedArgs:
                     testUrl = testUrl.replace("&", "\\&")
-                args.append(testUrl)
+                self.start_script_args.append(testUrl)
+            else:
+                self.start_script_args.append('about:blank')
 
             if detectShutdownLeaks:
                 shutdownLeaks = ShutdownLeaks(self.log)
             else:
                 shutdownLeaks = None
 
             if mozinfo.info["asan"] and (mozinfo.isLinux or mozinfo.isMac):
                 lsanLeaks = LSANLeaks(self.log)
@@ -1958,37 +1958,50 @@ class MochitestDesktop(MochitestBase):
                     'toolkit') != 'gonk':
                 runner_cls = mozrunner.Runner
             else:
                 runner_cls = mozrunner.runners.get(
                     mozinfo.info.get(
                         'appname',
                         'firefox'),
                     mozrunner.Runner)
+
             runner = runner_cls(profile=self.profile,
                                 binary=cmd,
                                 cmdargs=args,
                                 env=env,
                                 process_class=mozprocess.ProcessHandlerMixin,
                                 process_args=kp_kwargs)
 
             # start the runner
             runner.start(debug_args=debug_args,
                          interactive=interactive,
                          outputTimeout=timeout)
             proc = runner.process_handler
             self.log.info("runtests.py | Application pid: %d" % proc.pid)
             self.log.process_start("Main app process")
 
-            if onLaunch is not None:
-                # Allow callers to specify an onLaunch callback to be fired after the
-                # app is launched.
-                # We call onLaunch for b2g desktop mochitests so that we can
-                # run a Marionette script after gecko has completed startup.
-                onLaunch()
+            # start marionette and kick off the tests
+            marionette_args = marionette_args or {}
+            self.marionette = Marionette(**marionette_args)
+            self.marionette.start_session()
+
+            # install specialpowers and mochikit as temporary addons
+            addons = Addons(self.marionette)
+
+            if mozinfo.info.get('toolkit') != 'gonk':
+                addons.install(os.path.join(here, 'extensions', 'specialpowers'), temp=True)
+                addons.install(self.mochijar, temp=True)
+
+            self.execute_start_script()
+
+            # an open marionette session interacts badly with mochitest,
+            # delete it until we figure out why.
+            self.marionette.delete_session()
+            del self.marionette
 
             # wait until app is finished
             # XXX copy functionality from
             # https://github.com/mozilla/mozbase/blob/master/mozrunner/mozrunner/runner.py#L61
             # until bug 913970 is fixed regarding mozrunner `wait` not returning status
             # see https://bugzilla.mozilla.org/show_bug.cgi?id=913970
             status = proc.wait()
             self.log.process_exit("Main app process", status)
@@ -2081,34 +2094,34 @@ class MochitestDesktop(MochitestBase):
         testsToRun = []
         for test in tests:
             if 'disabled' in test:
                 continue
             testsToRun.append(test['path'])
 
         return testsToRun
 
-    def runMochitests(self, options, testsToRun, onLaunch=None):
+    def runMochitests(self, options, testsToRun):
         "This is a base method for calling other methods in this class for --bisect-chunk."
         # Making an instance of bisect class for --bisect-chunk option.
         bisect = bisection.Bisect(self)
         finished = False
         status = 0
         bisection_log = 0
         while not finished:
             if options.bisectChunk:
                 testsToRun = bisect.pre_test(options, testsToRun, status)
                 # To inform that we are in the process of bisection, and to
                 # look for bleedthrough
                 if options.bisectChunk != "default" and not bisection_log:
                     self.log.info(
                         "TEST-UNEXPECTED-FAIL | Bisection | Please ignore repeats and look for 'Bleedthrough' (if any) at the end of the failure list")
                     bisection_log = 1
 
-            result = self.doTests(options, onLaunch, testsToRun)
+            result = self.doTests(options, testsToRun)
             if options.bisectChunk:
                 status = bisect.post_test(
                     options,
                     self.expectedError,
                     self.result)
             else:
                 status = -1
 
@@ -2118,17 +2131,17 @@ class MochitestDesktop(MochitestBase):
         # We need to print the summary only if options.bisectChunk has a value.
         # Also we need to make sure that we do not print the summary in between
         # running tests via --run-by-dir.
         if options.bisectChunk and options.bisectChunk in self.result:
             bisect.print_summary()
 
         return result
 
-    def runTests(self, options, onLaunch=None):
+    def runTests(self, options):
         """ Prepare, configure, run tests and cleanup """
 
         self.setTestRoot(options)
 
         # Despite our efforts to clean up servers started by this script, in practice
         # we still see infrequent cases where a process is orphaned and interferes
         # with future tests, typically because the old server is keeping the port in use.
         # Try to avoid those failures by checking for and killing orphan servers before
@@ -2136,30 +2149,30 @@ class MochitestDesktop(MochitestBase):
         self.killNamedOrphans('ssltunnel')
         self.killNamedOrphans('xpcshell')
 
         # Until we have all green, this only runs on bc*/dt*/mochitest-chrome
         # jobs, not webapprt*, jetpack*, a11yr (for perf reasons), or plain
 
         testsToRun = self.getTestsToRun(options)
         if not options.runByDir:
-            return self.runMochitests(options, testsToRun, onLaunch)
+            return self.runMochitests(options, testsToRun)
 
         # code for --run-by-dir
         dirs = self.getDirectories(options)
 
         result = 1  # default value, if no tests are run.
         for d in dirs:
             print "dir: %s" % d
             tests_in_dir = [t for t in testsToRun if os.path.dirname(t) == d]
 
             # If we are using --run-by-dir, we should not use the profile path (if) provided
             # by the user, since we need to create a new directory for each run. We would face problems
             # if we use the directory provided by the user.
-            result = self.runMochitests(options, tests_in_dir, onLaunch)
+            result = self.runMochitests(options, tests_in_dir)
 
             # Dump the logging buffer
             self.message_logger.dump_buffered()
 
             if result == -1:
                 break
 
         # printing total number of tests
@@ -2174,17 +2187,17 @@ class MochitestDesktop(MochitestBase):
             print "0 INFO TEST-START | Shutdown"
             print "1 INFO Passed:  %s" % self.countpass
             print "2 INFO Failed:  %s" % self.countfail
             print "3 INFO Todo:    %s" % self.counttodo
             print "4 INFO SimpleTest FINISHED"
 
         return result
 
-    def doTests(self, options, onLaunch=None, testsToFilter=None):
+    def doTests(self, options, testsToFilter=None):
         # A call to initializeLooping method is required in case of --run-by-dir or --bisect-chunk
         # since we need to initialize variables for each loop.
         if options.bisectChunk or options.runByDir:
             self.initializeLooping(options)
 
         # get debugger info, a dict of:
         # {'path': path to the debugger (string),
         #  'interactive': whether the debugger is interactive or not (bool)
@@ -2312,35 +2325,45 @@ class MochitestDesktop(MochitestBase):
                 timeout = None
             else:
                 timeout = 330.0  # default JS harness timeout is 300 seconds
 
             # detect shutdown leaks for m-bc runs
             detectShutdownLeaks = mozinfo.info[
                 "debug"] and options.browserChrome and not options.webapprtChrome
 
+            self.start_script_args.append(self.getTestFlavor(options))
+            marionette_args = {
+                'symbols_path': options.symbolsPath,
+            }
+
+            if options.marionette:
+                host, port = options.marionette.split(':')
+                marionette_args['host'] = host
+                marionette_args['port'] = int(port)
+
             self.log.info("runtests.py | Running tests: start.\n")
             try:
                 status = self.runApp(testURL,
                                      self.browserEnv,
                                      options.app,
                                      profile=self.profile,
                                      extraArgs=options.browserArgs,
                                      utilityPath=options.utilityPath,
                                      debuggerInfo=debuggerInfo,
                                      valgrindPath=valgrindPath,
                                      valgrindArgs=valgrindArgs,
                                      valgrindSuppFiles=valgrindSuppFiles,
                                      symbolsPath=options.symbolsPath,
                                      timeout=timeout,
-                                     onLaunch=onLaunch,
                                      detectShutdownLeaks=detectShutdownLeaks,
                                      screenshotOnFail=options.screenshotOnFail,
                                      bisectChunk=options.bisectChunk,
-                                     quiet=options.quiet
+                                     quiet=options.quiet,
+                                     marionette_args=marionette_args,
                                      )
             except KeyboardInterrupt:
                 self.log.info("runtests.py | Received keyboard interrupt.\n")
                 status = -1
             except:
                 traceback.print_exc()
                 self.log.error(
                     "Automation Error: Received unexpected exception while running application\n")
@@ -2573,16 +2596,17 @@ class MochitestDesktop(MochitestBase):
 
         return dirlist
 
 
 def run_test_harness(options):
     logger_options = {
         key: value for key, value in vars(options).iteritems()
         if key.startswith('log') or key == 'valgrind'}
+
     runner = MochitestDesktop(logger_options)
 
     options.runByDir = False
 
     if runner.getTestFlavor(options) == 'mochitest':
         options.runByDir = True
 
     if runner.getTestFlavor(options) == 'browser-chrome':
--- a/testing/mochitest/runtestsb2g.py
+++ b/testing/mochitest/runtestsb2g.py
@@ -37,18 +37,18 @@ class MochitestB2G(MochitestBase):
                  remote_test_root=None,
                  remote_log_file=None):
         MochitestBase.__init__(self, logger_options)
         self.marionette_args = marionette_args
         self.out_of_process = out_of_process
         self.locations_file = locations
         self.preferences = []
         self.webapps = None
-        self.test_script = os.path.join(here, 'b2g_start_script.js')
-        self.test_script_args = [self.out_of_process]
+        self.start_script = os.path.join(here, 'start_b2g.js')
+        self.start_script_args = [self.out_of_process]
         self.product = 'b2g'
         self.remote_chrome_test_dir = None
         self.local_log = None
         self.local_binary_dir = local_binary_dir
 
         self.preferences = [
             os.path.join(
                 profile_data_dir,
@@ -193,19 +193,19 @@ class MochitestB2G(MochitestBase):
             # style manifest. Not so with B2G, that conversion along with updating the URL
             # option will happen later. So backup and restore options.manifestFile to
             # prevent us from trying to pass in an instance of TestManifest via url param.
             manifestFile = options.manifestFile
             options.manifestFile = None
             self.buildURLOptions(options, {'MOZ_HIDE_RESULTS_TABLE': '1'})
             options.manifestFile = manifestFile
 
-            self.test_script_args.append(not options.emulator)
-            self.test_script_args.append(options.wifi)
-            self.test_script_args.append(options.chrome)
+            self.start_script_args.append(not options.emulator)
+            self.start_script_args.append(options.wifi)
+            self.start_script_args.append(options.chrome)
 
             self.runner.start(outputTimeout=timeout)
 
             self.marionette.wait_for_port()
             self.marionette.start_session()
             self.marionette.set_context(self.marionette.CONTEXT_CHROME)
 
             # Disable offline status management (bug 777145), otherwise the network
@@ -238,25 +238,17 @@ class MochitestB2G(MochitestBase):
                 local = MochitestBase.getChromeTestDir(self, options)
                 local = os.path.join(local, "chrome")
                 remote = self.remote_chrome_test_dir
                 self.log.info(
                     "pushing %s to %s on device..." %
                     (local, remote))
                 self.app_ctx.dm.pushDir(local, remote)
 
-            if os.path.isfile(self.test_script):
-                with open(self.test_script, 'r') as script:
-                    self.marionette.execute_script(
-                        script.read(),
-                        script_args=self.test_script_args)
-            else:
-                self.marionette.execute_script(
-                    self.test_script,
-                    script_args=self.test_script_args)
+            self.execute_start_script()
             status = self.runner.wait()
 
             if status is None:
                 # the runner has timed out
                 status = 124
 
             local_leak_file = tempfile.NamedTemporaryFile()
             self.app_ctx.dm.getFile(
@@ -357,17 +349,17 @@ class MochitestB2G(MochitestBase):
         # For B2G emulators buildURLOptions has been called
         # without calling buildTestPath first and that
         # causes manifestFile not to be set
         if "manifestFile=tests.json" not in self.urlOpts:
             self.urlOpts.append("manifestFile=%s" % options.manifestFile)
 
         if len(self.urlOpts) > 0:
             test_url += "?" + "&".join(self.urlOpts)
-        self.test_script_args.append(test_url)
+        self.start_script_args.append(test_url)
 
         options.profilePath = self.app_ctx.remote_profile
         options.logFile = self.local_log
 
 
 def run_test_harness(options):
     # create our Marionette instance
     marionette_args = {
--- a/testing/mochitest/runtestsremote.py
+++ b/testing/mochitest/runtestsremote.py
@@ -191,16 +191,30 @@ class MochiRemote(MochitestDesktop):
             self.log.error(
                 "Automation Error: Unable to copy profile to device.")
             raise
 
         restoreRemotePaths()
         options.profilePath = self.remoteProfile
         return manifest
 
+    def addChromeToProfile(self, options):
+        manifest = MochitestDesktop.addChromeToProfile(self, options)
+
+        # Support Firefox (browser), B2G (shell), SeaMonkey (navigator), and Webapp
+        # Runtime (webapp).
+        if options.chrome:
+            # append overlay to chrome.manifest
+            chrome = "overlay chrome://browser/content/browser.xul chrome://mochikit/content/browser-test-overlay.xul"
+            path = os.path.join(options.profilePath, 'extensions', 'staged',
+                                'mochikit@mozilla.org', 'chrome.manifest')
+            with open(path, "a") as f:
+                f.write(chrome)
+        return manifest
+
     def buildURLOptions(self, options, env):
         self.localLog = options.logFile
         options.logFile = self.remoteLog
         options.fileLevel = 'INFO'
         options.profilePath = self.localProfile
         env["MOZ_HIDE_RESULTS_TABLE"] = "1"
         retVal = MochitestDesktop.buildURLOptions(self, options, env)
 
@@ -276,18 +290,19 @@ class MochiRemote(MochitestDesktop):
     def runApp(self, *args, **kwargs):
         """front-end automation.py's `runApp` functionality until FennecRunner is written"""
 
         # automation.py/remoteautomation `runApp` takes the profile path,
         # whereas runtest.py's `runApp` takes a mozprofile object.
         if 'profileDir' not in kwargs and 'profile' in kwargs:
             kwargs['profileDir'] = kwargs.pop('profile').profile
 
-        if 'quiet' in kwargs:
-            kwargs.pop('quiet')
+        # remove args not supported by automation.py
+        kwargs.pop('marionette_args', None)
+        kwargs.pop('quiet', None)
 
         return self._automation.runApp(*args, **kwargs)
 
 
 def run_test_harness(options):
     message_logger = MessageLogger(logger=None)
     process_args = {'messageLogger': message_logger}
     auto = RemoteAutomation(None, "fennec", processArgs=process_args)
rename from testing/mochitest/b2g_start_script.js
rename to testing/mochitest/start_b2g.js
new file mode 100644
--- /dev/null
+++ b/testing/mochitest/start_desktop.js
@@ -0,0 +1,15 @@
+/* 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/. */
+
+const flavor  = __marionetteParams[0]
+const url = __marionetteParams[1]
+
+let wm = Cc["@mozilla.org/appshell/window-mediator;1"]
+          .getService(Ci.nsIWindowMediator);
+let win = wm.getMostRecentWindow("navigator:browser");
+
+// mochikit's bootstrap.js has set up a listener for this event. It's
+// used so bootstrap.js knows which flavor and url to load.
+let ev = new CustomEvent('mochitest-load', {'detail': [flavor, url]});
+win.dispatchEvent(ev);
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -88,16 +88,18 @@ user_pref("browser.safebrowsing.download
 user_pref("browser.safebrowsing.provider.google.gethashURL", "http://%(server)s/safebrowsing-dummy/gethash");
 user_pref("browser.safebrowsing.provider.google.updateURL", "http://%(server)s/safebrowsing-dummy/update");
 user_pref("browser.safebrowsing.provider.mozilla.gethashURL", "http://%(server)s/safebrowsing-dummy/gethash");
 user_pref("browser.safebrowsing.provider.mozilla.updateURL", "http://%(server)s/safebrowsing-dummy/update");
 user_pref("privacy.trackingprotection.introURL", "http://%(server)s/trackingprotection/tour");
 // Point update checks to the local testing server for fast failures
 user_pref("extensions.update.url", "http://%(server)s/extensions-dummy/updateURL");
 user_pref("extensions.update.background.url", "http://%(server)s/extensions-dummy/updateBackgroundURL");
+user_pref("extensions.blocklist.detailsURL", "http://%(server)s/extensions-dummy/blocklistDetailsURL");
+user_pref("extensions.blocklist.itemURL", "http://%(server)s/extensions-dummy/blocklistItemURL");
 user_pref("extensions.blocklist.url", "http://%(server)s/extensions-dummy/blocklistURL");
 user_pref("extensions.hotfix.url", "http://%(server)s/extensions-dummy/hotfixURL");
 user_pref("extensions.systemAddon.update.url", "http://%(server)s/dummy-system-addons.xml");
 // Turn off extension updates so they don't bother tests
 user_pref("extensions.update.enabled", false);
 // Bug 1235627 Turn off pocket add-on so it doesn't bother tests
 user_pref("extensions.pocket.enabled", false);
 // Make sure opening about:addons won't hit the network
@@ -338,8 +340,9 @@ user_pref("browser.urlbar.suggest.search
 
 // Turn off the location bar search suggestions opt-in.  It interferes with
 // tests that don't expect it to be there.
 user_pref("browser.urlbar.userMadeSearchSuggestionsChoice", true);
 
 user_pref("dom.audiochannel.mutedByDefault", false);
 
 user_pref("webextensions.tests", true);
+user_pref("startup.homepage_welcome_url", "about:blank");