Bug 1513880 [wpt PR 14496] - Revert "Make testharness tests run in a top-level browsing context", a=testonly
authormoz-wptsync-bot <wptsync@mozilla.com>
Wed, 19 Dec 2018 11:16:37 +0000
changeset 515012 0f0edff319bf9938e6ce47e62c586d2f25919263
parent 515011 ecd09e388677f29f2b802e100dac10fd7c6e1f16
child 515013 0b60b97c45be092bda5346f57ebdcd0c5be3caa7
push id1953
push userffxbld-merge
push dateMon, 11 Mar 2019 12:10:20 +0000
treeherdermozilla-release@9c35dcbaa899 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstestonly
bugs1513880, 14496, 14495, 13418
milestone66.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 1513880 [wpt PR 14496] - Revert "Make testharness tests run in a top-level browsing context", a=testonly Automatic update from web-platform-tests Revert "Make testharness tests run in a top-level browsing context" (#14496) This reverts commit 74522a275bea481821e789145578e9e16fd27be3. Fixes https://github.com/web-platform-tests/wpt/issues/14495. Reopens https://github.com/web-platform-tests/wpt/issues/13418. -- wpt-commits: 9377b490049c9af88ea0261ad3b92757733e8e95 wpt-pr: 14496
testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/base.py
testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/base.py.orig
testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/chrome.py
testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/chrome_android.py
testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/edge.py
testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/fennec.py
testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/fennec.py.orig
testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox.py
testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/ie.py
testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/opera.py
testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/safari.py
testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/sauce.py
testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/sauce.py.orig
testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/servo.py
testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/servodriver.py
testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/webkit.py
testing/web-platform/tests/tools/wptrunner/wptrunner/environment.py
testing/web-platform/tests/tools/wptrunner/wptrunner/executors/base.py
testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executormarionette.py
testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executormarionette.py.orig
testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executorselenium.py
testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executorwebdriver.py
testing/web-platform/tests/tools/wptrunner/wptrunner/executors/runner.js
testing/web-platform/tests/tools/wptrunner/wptrunner/executors/testharness_webdriver.js
testing/web-platform/tests/tools/wptrunner/wptrunner/executors/testharness_webdriver_resume.js
testing/web-platform/tests/tools/wptrunner/wptrunner/products.py
testing/web-platform/tests/tools/wptrunner/wptrunner/testdriver-extra.js
testing/web-platform/tests/tools/wptrunner/wptrunner/testharnessreport.js
testing/web-platform/tests/tools/wptrunner/wptrunner/tests/test_products.py
testing/web-platform/tests/tools/wptrunner/wptrunner/wptrunner.py
testing/web-platform/tests/tools/wptserve/wptserve/stash.py
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/base.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/base.py
@@ -11,17 +11,17 @@ here = os.path.split(__file__)[0]
 
 def inherit(super_module, child_globals, product_name):
     super_wptrunner = super_module.__wptrunner__
     child_globals["__wptrunner__"] = child_wptrunner = deepcopy(super_wptrunner)
 
     child_wptrunner["product"] = product_name
 
     for k in ("check_args", "browser", "browser_kwargs", "executor_kwargs",
-              "env_extras", "env_options", "timeout_multiplier"):
+              "env_extras", "env_options"):
         attr = super_wptrunner[k]
         child_globals[attr] = getattr(super_module, attr)
 
     for v in super_module.__wptrunner__["executor"].values():
         child_globals[v] = getattr(super_module, v)
 
     if "run_info_extras" in super_wptrunner:
         attr = super_wptrunner["run_info_extras"]
@@ -76,23 +76,16 @@ def get_free_port(start_port, exclude=No
             s.bind(("127.0.0.1", port))
         except socket.error:
             port += 1
         else:
             return port
         finally:
             s.close()
 
-
-def get_timeout_multiplier(test_type, run_info_data, **kwargs):
-    if kwargs["timeout_multiplier"] is not None:
-        return kwargs["timeout_multiplier"]
-    return 1
-
-
 def browser_command(binary, args, debug_info):
     if debug_info:
         if debug_info.requiresEscapedArgs:
             args = [item.replace("&", "\\&") for item in args]
         debug_args = [debug_info.path] + debug_info.args
     else:
         debug_args = []
 
copy from testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/base.py
copy to testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/base.py.orig
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/chrome.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/chrome.py
@@ -1,28 +1,26 @@
 from .base import Browser, ExecutorBrowser, require_arg
-from .base import get_timeout_multiplier   # noqa: F401
 from ..webdriver_server import ChromeDriverServer
 from ..executors import executor_kwargs as base_executor_kwargs
 from ..executors.executorwebdriver import (WebDriverTestharnessExecutor,  # noqa: F401
                                            WebDriverRefTestExecutor)  # noqa: F401
 from ..executors.executorchrome import ChromeDriverWdspecExecutor  # noqa: F401
 
 
 __wptrunner__ = {"product": "chrome",
                  "check_args": "check_args",
                  "browser": "ChromeBrowser",
                  "executor": {"testharness": "WebDriverTestharnessExecutor",
                               "reftest": "WebDriverRefTestExecutor",
                               "wdspec": "ChromeDriverWdspecExecutor"},
                  "browser_kwargs": "browser_kwargs",
                  "executor_kwargs": "executor_kwargs",
                  "env_extras": "env_extras",
-                 "env_options": "env_options",
-                 "timeout_multiplier": "get_timeout_multiplier",}
+                 "env_options": "env_options"}
 
 
 def check_args(**kwargs):
     require_arg(kwargs, "webdriver_binary")
 
 
 def browser_kwargs(test_type, run_info_data, config, **kwargs):
     return {"binary": kwargs["binary"],
@@ -31,34 +29,30 @@ def browser_kwargs(test_type, run_info_d
 
 
 def executor_kwargs(test_type, server_config, cache_manager, run_info_data,
                     **kwargs):
     executor_kwargs = base_executor_kwargs(test_type, server_config,
                                            cache_manager, run_info_data,
                                            **kwargs)
     executor_kwargs["close_after_done"] = True
-    executor_kwargs["supports_eager_pageload"] = False
 
     capabilities = {
         "goog:chromeOptions": {
             "prefs": {
                 "profile": {
                     "default_content_setting_values": {
                         "popups": 1
                     }
                 }
             },
             "w3c": True
         }
     }
 
-    if test_type == "testharness":
-        capabilities["pageLoadStrategy"] = "none"
-
     for (kwarg, capability) in [("binary", "binary"), ("binary_args", "args")]:
         if kwargs[kwarg] is not None:
             capabilities["goog:chromeOptions"][capability] = kwargs[kwarg]
 
     if kwargs["headless"]:
         if "args" not in capabilities["goog:chromeOptions"]:
             capabilities["goog:chromeOptions"]["args"] = []
         if "--headless" not in capabilities["goog:chromeOptions"]["args"]:
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/chrome_android.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/chrome_android.py
@@ -1,30 +1,28 @@
 import subprocess
 
 from .base import Browser, ExecutorBrowser, require_arg
-from .base import get_timeout_multiplier   # noqa: F401
 from ..webdriver_server import ChromeDriverServer
 from ..executors import executor_kwargs as base_executor_kwargs
 from ..executors.executorselenium import (SeleniumTestharnessExecutor,  # noqa: F401
                                           SeleniumRefTestExecutor)  # noqa: F401
 from ..executors.executorchrome import ChromeDriverWdspecExecutor  # noqa: F401
 
 
 __wptrunner__ = {"product": "chrome_android",
                  "check_args": "check_args",
                  "browser": "ChromeAndroidBrowser",
                  "executor": {"testharness": "SeleniumTestharnessExecutor",
                               "reftest": "SeleniumRefTestExecutor",
                               "wdspec": "ChromeDriverWdspecExecutor"},
                  "browser_kwargs": "browser_kwargs",
                  "executor_kwargs": "executor_kwargs",
                  "env_extras": "env_extras",
-                 "env_options": "env_options",
-                 "timeout_multiplier": "get_timeout_multiplier"}
+                 "env_options": "env_options"}
 
 _wptserve_ports = set()
 
 
 def check_args(**kwargs):
     require_arg(kwargs, "webdriver_binary")
 
 
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/edge.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/edge.py
@@ -9,62 +9,52 @@ from ..executors.executoredge import Edg
                  "check_args": "check_args",
                  "browser": "EdgeBrowser",
                  "executor": {"testharness": "SeleniumTestharnessExecutor",
                               "reftest": "SeleniumRefTestExecutor",
                               "wdspec": "EdgeDriverWdspecExecutor"},
                  "browser_kwargs": "browser_kwargs",
                  "executor_kwargs": "executor_kwargs",
                  "env_extras": "env_extras",
-                 "env_options": "env_options",
-                 "timeout_multiplier": "get_timeout_multiplier"}
-
+                 "env_options": "env_options"}
 
 def get_timeout_multiplier(test_type, run_info_data, **kwargs):
     if kwargs["timeout_multiplier"] is not None:
         return kwargs["timeout_multiplier"]
     if test_type == "wdspec":
         return 10
     return 1
 
-
 def check_args(**kwargs):
     require_arg(kwargs, "webdriver_binary")
 
-
 def browser_kwargs(test_type, run_info_data, config, **kwargs):
     return {"webdriver_binary": kwargs["webdriver_binary"],
             "webdriver_args": kwargs.get("webdriver_args"),
             "timeout_multiplier": get_timeout_multiplier(test_type,
                                                          run_info_data,
                                                          **kwargs)}
 
-
 def executor_kwargs(test_type, server_config, cache_manager, run_info_data,
                     **kwargs):
     executor_kwargs = base_executor_kwargs(test_type, server_config,
                                            cache_manager, run_info_data, **kwargs)
     executor_kwargs["close_after_done"] = True
     executor_kwargs["timeout_multiplier"] = get_timeout_multiplier(test_type,
                                                                    run_info_data,
                                                                    **kwargs)
     executor_kwargs["capabilities"] = {}
-    if test_type == "testharness":
-        executor_kwargs["capabilities"]["pageLoadStrategy"] = "eager"
     return executor_kwargs
 
-
 def env_extras(**kwargs):
     return []
 
-
 def env_options():
     return {"supports_debugger": False}
 
-
 class EdgeBrowser(Browser):
     used_ports = set()
     init_timeout = 60
 
     def __init__(self, logger, webdriver_binary, timeout_multiplier=None, webdriver_args=None):
         Browser.__init__(self, logger)
         self.server = EdgeDriverServer(self.logger,
                                        binary=webdriver_binary,
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/fennec.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/fennec.py
@@ -24,19 +24,17 @@ from .firefox import (get_timeout_multip
                  "browser": "FennecBrowser",
                  "executor": {"testharness": "MarionetteTestharnessExecutor",
                               "reftest": "MarionetteRefTestExecutor"},
                  "browser_kwargs": "browser_kwargs",
                  "executor_kwargs": "executor_kwargs",
                  "env_extras": "env_extras",
                  "env_options": "env_options",
                  "run_info_extras": "run_info_extras",
-                 "update_properties": "update_properties",
-                 "timeout_multiplier": "get_timeout_multiplier"}
-
+                 "update_properties": "update_properties"}
 
 
 def check_args(**kwargs):
     pass
 
 
 def browser_kwargs(test_type, run_info_data, config, **kwargs):
     return {"package_name": kwargs["package_name"],
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/fennec.py.orig
@@ -0,0 +1,219 @@
+import os
+import tempfile
+
+import moznetwork
+from mozprocess import ProcessHandler
+from mozprofile import FirefoxProfile
+from mozrunner import FennecEmulatorRunner
+
+from tools.serve.serve import make_hosts_file
+
+from .base import (get_free_port,
+                   cmd_arg,
+                   browser_command)
+from ..executors.executormarionette import (MarionetteTestharnessExecutor,  # noqa: F401
+                                            MarionetteRefTestExecutor)  # noqa: F401
+from .firefox import (get_timeout_multiplier,
+                      update_properties,
+                      executor_kwargs,
+                      FirefoxBrowser)
+
+
+__wptrunner__ = {"product": "fennec",
+                 "check_args": "check_args",
+                 "browser": "FennecBrowser",
+                 "executor": {"testharness": "MarionetteTestharnessExecutor",
+                              "reftest": "MarionetteRefTestExecutor"},
+                 "browser_kwargs": "browser_kwargs",
+                 "executor_kwargs": "executor_kwargs",
+                 "env_extras": "env_extras",
+                 "env_options": "env_options",
+                 "run_info_extras": "run_info_extras",
+                 "update_properties": "update_properties",
+                 "timeout_multiplier": "get_timeout_multiplier"}
+
+
+
+def check_args(**kwargs):
+    pass
+
+
+def browser_kwargs(test_type, run_info_data, config, **kwargs):
+    return {"package_name": kwargs["package_name"],
+            "device_serial": kwargs["device_serial"],
+            "prefs_root": kwargs["prefs_root"],
+            "extra_prefs": kwargs["extra_prefs"],
+            "test_type": test_type,
+            "debug_info": kwargs["debug_info"],
+            "symbols_path": kwargs["symbols_path"],
+            "stackwalk_binary": kwargs["stackwalk_binary"],
+            "certutil_binary": kwargs["certutil_binary"],
+            "ca_certificate_path": config.ssl_config["ca_cert_path"],
+            "stackfix_dir": kwargs["stackfix_dir"],
+            "binary_args": kwargs["binary_args"],
+            "timeout_multiplier": get_timeout_multiplier(test_type,
+                                                         run_info_data,
+                                                         **kwargs),
+            "leak_check": kwargs["leak_check"],
+            "stylo_threads": kwargs["stylo_threads"],
+            "chaos_mode_flags": kwargs["chaos_mode_flags"],
+            "config": config,
+            "install_fonts": kwargs["install_fonts"],
+            "tests_root": config.doc_root}
+
+
+def env_extras(**kwargs):
+    return []
+
+
+def run_info_extras(**kwargs):
+    return {"e10s": False,
+            "headless": False,
+            "sw-e10s": False}
+
+
+def env_options():
+    # The server host is set to public localhost IP so that resources can be accessed
+    # from Android emulator
+    return {"server_host": moznetwork.get_ip(),
+            "bind_address": False,
+            "supports_debugger": True}
+
+
+def write_hosts_file(config, device):
+    new_hosts = make_hosts_file(config, moznetwork.get_ip())
+    current_hosts = device.get_file("/etc/hosts")
+    if new_hosts == current_hosts:
+        return
+    hosts_fd, hosts_path = tempfile.mkstemp()
+    try:
+        with os.fdopen(hosts_fd, "w") as f:
+            f.write(new_hosts)
+        device.remount()
+        device.push(hosts_path, "/etc/hosts")
+    finally:
+        os.remove(hosts_path)
+
+
+class FennecBrowser(FirefoxBrowser):
+    used_ports = set()
+    init_timeout = 300
+    shutdown_timeout = 60
+
+    def __init__(self, logger, prefs_root, test_type, package_name=None,
+                 device_serial="emulator-5444", **kwargs):
+        FirefoxBrowser.__init__(self, logger, None, prefs_root, test_type, **kwargs)
+        self._package_name = package_name
+        self.device_serial = device_serial
+        self.tests_root = kwargs["tests_root"]
+        self.install_fonts = kwargs["install_fonts"]
+
+    @property
+    def package_name(self):
+        """
+        Name of app to run on emulator.
+        """
+        if self._package_name is None:
+            self._package_name = "org.mozilla.fennec"
+            user = os.getenv("USER")
+            if user:
+                self._package_name += "_" + user
+        return self._package_name
+
+    def start(self, **kwargs):
+        if self.marionette_port is None:
+            self.marionette_port = get_free_port(2828, exclude=self.used_ports)
+            self.used_ports.add(self.marionette_port)
+
+        env = {}
+        env["MOZ_CRASHREPORTER"] = "1"
+        env["MOZ_CRASHREPORTER_SHUTDOWN"] = "1"
+        env["MOZ_DISABLE_NONLOCAL_CONNECTIONS"] = "1"
+        env["STYLO_THREADS"] = str(self.stylo_threads)
+        if self.chaos_mode_flags is not None:
+            env["MOZ_CHAOSMODE"] = str(self.chaos_mode_flags)
+
+        preferences = self.load_prefs()
+
+        self.profile = FirefoxProfile(preferences=preferences)
+        self.profile.set_preferences({"marionette.port": self.marionette_port,
+                                      "dom.disable_open_during_load": False,
+                                      "places.history.enabled": False,
+                                      "dom.send_after_paint_to_content": True,
+                                      "network.preload": True})
+        if self.test_type == "reftest":
+            self.logger.info("Setting android reftest preferences")
+            self.profile.set_preferences({"browser.viewport.desktopWidth": 600,
+                                          # Disable high DPI
+                                          "layout.css.devPixelsPerPx": "1.0",
+                                          # Ensure that the full browser element
+                                          # appears in the screenshot
+                                          "apz.allow_zooming": False,
+                                          "android.widget_paints_background": False,
+                                          # Ensure that scrollbars are always painted
+                                          "ui.scrollbarFadeBeginDelay": 100000})
+
+        if self.install_fonts:
+            self.logger.debug("Copying Ahem font to profile")
+            font_dir = os.path.join(self.profile.profile, "fonts")
+            if not os.path.exists(font_dir):
+                os.makedirs(font_dir)
+            with open(os.path.join(self.tests_root, "fonts", "Ahem.ttf"), "rb") as src:
+                with open(os.path.join(font_dir, "Ahem.ttf"), "wb") as dest:
+                    dest.write(src.read())
+
+        if self.leak_check and kwargs.get("check_leaks", True):
+            self.leak_report_file = os.path.join(self.profile.profile, "runtests_leaks.log")
+            if os.path.exists(self.leak_report_file):
+                os.remove(self.leak_report_file)
+            env["XPCOM_MEM_BLOAT_LOG"] = self.leak_report_file
+        else:
+            self.leak_report_file = None
+
+        if self.ca_certificate_path is not None:
+            self.setup_ssl()
+
+        debug_args, cmd = browser_command(self.package_name,
+                                          self.binary_args if self.binary_args else [] +
+                                          [cmd_arg("marionette"), "about:blank"],
+                                          self.debug_info)
+
+        self.runner = FennecEmulatorRunner(app=self.package_name,
+                                           profile=self.profile,
+                                           cmdargs=cmd[1:],
+                                           env=env,
+                                           symbols_path=self.symbols_path,
+                                           serial=self.device_serial,
+                                           # TODO - choose appropriate log dir
+                                           logdir=os.getcwd(),
+                                           process_class=ProcessHandler,
+                                           process_args={"processOutputLine": [self.on_output]})
+
+        self.logger.debug("Starting %s" % self.package_name)
+        # connect to a running emulator
+        self.runner.device.connect()
+
+        write_hosts_file(self.config, self.runner.device.device)
+
+        self.runner.stop()
+        self.runner.start(debug_args=debug_args, interactive=self.debug_info and self.debug_info.interactive)
+
+        self.runner.device.device.forward(
+            local="tcp:{}".format(self.marionette_port),
+            remote="tcp:{}".format(self.marionette_port))
+
+        self.logger.debug("%s Started" % self.package_name)
+
+    def stop(self, force=False):
+        if self.runner is not None:
+            if (self.runner.device.connected and
+                len(self.runner.device.device.list_forwards()) > 0):
+                try:
+                    self.runner.device.device.remove_forwards(
+                        "tcp:{}".format(self.marionette_port))
+                except Exception:
+                    self.logger.warning("Failed to remove port forwarding")
+            # We assume that stopping the runner prompts the
+            # browser to shut down. This allows the leak log to be written
+            self.runner.stop()
+        self.logger.debug("stopped")
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox.py
@@ -33,18 +33,17 @@ here = os.path.join(os.path.split(__file
                  "executor": {"testharness": "MarionetteTestharnessExecutor",
                               "reftest": "MarionetteRefTestExecutor",
                               "wdspec": "MarionetteWdspecExecutor"},
                  "browser_kwargs": "browser_kwargs",
                  "executor_kwargs": "executor_kwargs",
                  "env_extras": "env_extras",
                  "env_options": "env_options",
                  "run_info_extras": "run_info_extras",
-                 "update_properties": "update_properties",
-                 "timeout_multiplier": "get_timeout_multiplier"}
+                 "update_properties": "update_properties"}
 
 
 def get_timeout_multiplier(test_type, run_info_data, **kwargs):
     if kwargs["timeout_multiplier"] is not None:
         return kwargs["timeout_multiplier"]
     if test_type == "reftest":
         if run_info_data["debug"] or run_info_data.get("asan"):
             return 4
@@ -95,18 +94,16 @@ def executor_kwargs(test_type, server_co
                                            cache_manager, run_info_data,
                                            **kwargs)
     executor_kwargs["close_after_done"] = test_type != "reftest"
     executor_kwargs["timeout_multiplier"] = get_timeout_multiplier(test_type,
                                                                    run_info_data,
                                                                    **kwargs)
     executor_kwargs["e10s"] = run_info_data["e10s"]
     capabilities = {}
-    if test_type == "testharness":
-        capabilities["pageLoadStrategy"] = "eager"
     if test_type == "reftest":
         executor_kwargs["reftest_internal"] = kwargs["reftest_internal"]
         executor_kwargs["reftest_screenshot"] = kwargs["reftest_screenshot"]
     if test_type == "wdspec":
         options = {}
         if kwargs["binary"]:
             options["binary"] = kwargs["binary"]
         if kwargs["binary_args"]:
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/ie.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/ie.py
@@ -1,27 +1,25 @@
 from .base import Browser, ExecutorBrowser, require_arg
-from .base import get_timeout_multiplier   # noqa: F401
 from ..webdriver_server import InternetExplorerDriverServer
 from ..executors import executor_kwargs as base_executor_kwargs
 from ..executors.executorselenium import (SeleniumTestharnessExecutor,  # noqa: F401
                                           SeleniumRefTestExecutor)  # noqa: F401
 from ..executors.executorinternetexplorer import InternetExplorerDriverWdspecExecutor  # noqa: F401
 
 __wptrunner__ = {"product": "ie",
                  "check_args": "check_args",
                  "browser": "InternetExplorerBrowser",
                  "executor": {"testharness": "SeleniumTestharnessExecutor",
                               "reftest": "SeleniumRefTestExecutor",
                               "wdspec": "InternetExplorerDriverWdspecExecutor"},
                  "browser_kwargs": "browser_kwargs",
                  "executor_kwargs": "executor_kwargs",
                  "env_extras": "env_extras",
-                 "env_options": "env_options",
-                 "timeout_multiplier": "get_timeout_multiplier"}
+                 "env_options": "env_options"}
 
 
 def check_args(**kwargs):
     require_arg(kwargs, "webdriver_binary")
 
 def browser_kwargs(test_type, run_info_data, config, **kwargs):
     return {"webdriver_binary": kwargs["webdriver_binary"],
             "webdriver_args": kwargs.get("webdriver_args")}
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/opera.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/opera.py
@@ -1,28 +1,26 @@
 from .base import Browser, ExecutorBrowser, require_arg
-from .base import get_timeout_multiplier   # noqa: F401
 from ..webdriver_server import OperaDriverServer
 from ..executors import executor_kwargs as base_executor_kwargs
 from ..executors.executorselenium import (SeleniumTestharnessExecutor,  # noqa: F401
                                           SeleniumRefTestExecutor)  # noqa: F401
 from ..executors.executoropera import OperaDriverWdspecExecutor  # noqa: F401
 
 
 __wptrunner__ = {"product": "opera",
                  "check_args": "check_args",
                  "browser": "OperaBrowser",
                  "executor": {"testharness": "SeleniumTestharnessExecutor",
                               "reftest": "SeleniumRefTestExecutor",
                               "wdspec": "OperaDriverWdspecExecutor"},
                  "browser_kwargs": "browser_kwargs",
                  "executor_kwargs": "executor_kwargs",
                  "env_extras": "env_extras",
-                 "env_options": "env_options",
-                 "timeout_multiplier": "get_timeout_multiplier"}
+                 "env_options": "env_options"}
 
 
 def check_args(**kwargs):
     require_arg(kwargs, "webdriver_binary")
 
 
 def browser_kwargs(test_type, run_info_data, config, **kwargs):
     return {"binary": kwargs["binary"],
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/safari.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/safari.py
@@ -1,28 +1,26 @@
 from .base import Browser, ExecutorBrowser, require_arg
-from .base import get_timeout_multiplier   # noqa: F401
 from ..webdriver_server import SafariDriverServer
 from ..executors import executor_kwargs as base_executor_kwargs
 from ..executors.executorwebdriver import (WebDriverTestharnessExecutor,  # noqa: F401
                                            WebDriverRefTestExecutor)  # noqa: F401
 from ..executors.executorsafari import SafariDriverWdspecExecutor  # noqa: F401
 
 
 __wptrunner__ = {"product": "safari",
                  "check_args": "check_args",
                  "browser": "SafariBrowser",
                  "executor": {"testharness": "WebDriverTestharnessExecutor",
                               "reftest": "WebDriverRefTestExecutor",
                               "wdspec": "SafariDriverWdspecExecutor"},
                  "browser_kwargs": "browser_kwargs",
                  "executor_kwargs": "executor_kwargs",
                  "env_extras": "env_extras",
-                 "env_options": "env_options",
-                 "timeout_multiplier": "get_timeout_multiplier"}
+                 "env_options": "env_options"}
 
 
 def check_args(**kwargs):
     require_arg(kwargs, "webdriver_binary")
 
 
 def browser_kwargs(test_type, run_info_data, config, **kwargs):
     return {"webdriver_binary": kwargs["webdriver_binary"],
@@ -30,18 +28,16 @@ def browser_kwargs(test_type, run_info_d
 
 
 def executor_kwargs(test_type, server_config, cache_manager, run_info_data,
                     **kwargs):
     executor_kwargs = base_executor_kwargs(test_type, server_config,
                                            cache_manager, run_info_data, **kwargs)
     executor_kwargs["close_after_done"] = True
     executor_kwargs["capabilities"] = {}
-    if test_type == "testharness":
-        executor_kwargs["capabilities"]["pageLoadStrategy"] = "eager"
     if kwargs["binary"] is not None:
         raise ValueError("Safari doesn't support setting executable location")
 
     return executor_kwargs
 
 
 def env_extras(**kwargs):
     return []
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/sauce.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/sauce.py
@@ -9,17 +9,16 @@ import subprocess
 import tarfile
 import tempfile
 import time
 from cStringIO import StringIO as CStringIO
 
 import requests
 
 from .base import Browser, ExecutorBrowser, require_arg
-from .base import get_timeout_multiplier   # noqa: F401
 from ..executors import executor_kwargs as base_executor_kwargs
 from ..executors.executorselenium import (SeleniumTestharnessExecutor,
                                           SeleniumRefTestExecutor)
 
 here = os.path.split(__file__)[0]
 # Number of seconds to wait between polling operations when detecting status of
 # Sauce Connect sub-process.
 sc_poll_period = 1
@@ -28,18 +27,17 @@ sc_poll_period = 1
 __wptrunner__ = {"product": "sauce",
                  "check_args": "check_args",
                  "browser": "SauceBrowser",
                  "executor": {"testharness": "SeleniumTestharnessExecutor",
                               "reftest": "SeleniumRefTestExecutor"},
                  "browser_kwargs": "browser_kwargs",
                  "executor_kwargs": "executor_kwargs",
                  "env_extras": "env_extras",
-                 "env_options": "env_options",
-                 "timeout_multiplier": "get_timeout_multiplier"}
+                 "env_options": "env_options"}
 
 
 def get_capabilities(**kwargs):
     browser_name = kwargs["sauce_browser"]
     platform = kwargs["sauce_platform"]
     version = kwargs["sauce_version"]
     build = kwargs["sauce_build"]
     tags = kwargs["sauce_tags"]
copy from testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/sauce.py
copy to testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/sauce.py.orig
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/servo.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/servo.py
@@ -1,12 +1,11 @@
 import os
 
 from .base import NullBrowser, ExecutorBrowser, require_arg
-from .base import get_timeout_multiplier   # noqa: F401
 from ..executors import executor_kwargs as base_executor_kwargs
 from ..executors.executorservo import ServoTestharnessExecutor, ServoRefTestExecutor, ServoWdspecExecutor  # noqa: F401
 
 here = os.path.join(os.path.split(__file__)[0])
 
 __wptrunner__ = {
     "product": "servo",
     "check_args": "check_args",
@@ -15,17 +14,16 @@ here = os.path.join(os.path.split(__file
         "testharness": "ServoTestharnessExecutor",
         "reftest": "ServoRefTestExecutor",
         "wdspec": "ServoWdspecExecutor",
     },
     "browser_kwargs": "browser_kwargs",
     "executor_kwargs": "executor_kwargs",
     "env_extras": "env_extras",
     "env_options": "env_options",
-    "timeout_multiplier": "get_timeout_multiplier",
     "update_properties": "update_properties",
 }
 
 
 def check_args(**kwargs):
     require_arg(kwargs, "binary")
 
 
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/servodriver.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/servodriver.py
@@ -2,17 +2,16 @@ import os
 import subprocess
 import tempfile
 
 from mozprocess import ProcessHandler
 
 from tools.serve.serve import make_hosts_file
 
 from .base import Browser, require_arg, get_free_port, browser_command, ExecutorBrowser
-from .base import get_timeout_multiplier   # noqa: F401
 from ..executors import executor_kwargs as base_executor_kwargs
 from ..executors.executorservodriver import (ServoWebDriverTestharnessExecutor,  # noqa: F401
                                              ServoWebDriverRefTestExecutor)  # noqa: F401
 
 here = os.path.join(os.path.split(__file__)[0])
 
 __wptrunner__ = {
     "product": "servodriver",
@@ -21,17 +20,16 @@ here = os.path.join(os.path.split(__file
     "executor": {
         "testharness": "ServoWebDriverTestharnessExecutor",
         "reftest": "ServoWebDriverRefTestExecutor",
     },
     "browser_kwargs": "browser_kwargs",
     "executor_kwargs": "executor_kwargs",
     "env_extras": "env_extras",
     "env_options": "env_options",
-    "timeout_multiplier": "get_timeout_multiplier",
     "update_properties": "update_properties",
 }
 
 
 def check_args(**kwargs):
     require_arg(kwargs, "binary")
 
 
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/webkit.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/webkit.py
@@ -1,10 +1,9 @@
 from .base import Browser, ExecutorBrowser, require_arg
-from .base import get_timeout_multiplier   # noqa: F401
 from ..executors import executor_kwargs as base_executor_kwargs
 from ..executors.executorwebdriver import (WebDriverTestharnessExecutor,  # noqa: F401
                                            WebDriverRefTestExecutor)  # noqa: F401
 from ..executors.executorwebkit import WebKitDriverWdspecExecutor  # noqa: F401
 from ..webdriver_server import WebKitDriverServer
 
 
 __wptrunner__ = {"product": "webkit",
@@ -12,18 +11,17 @@ from ..webdriver_server import WebKitDri
                  "browser": "WebKitBrowser",
                  "browser_kwargs": "browser_kwargs",
                  "executor": {"testharness": "WebDriverTestharnessExecutor",
                               "reftest": "WebDriverRefTestExecutor",
                               "wdspec": "WebKitDriverWdspecExecutor"},
                  "executor_kwargs": "executor_kwargs",
                  "env_extras": "env_extras",
                  "env_options": "env_options",
-                 "run_info_extras": "run_info_extras",
-                 "timeout_multiplier": "get_timeout_multiplier"}
+                 "run_info_extras": "run_info_extras"}
 
 
 def check_args(**kwargs):
     require_arg(kwargs, "binary")
     require_arg(kwargs, "webdriver_binary")
     require_arg(kwargs, "webkit_port")
 
 
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/environment.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/environment.py
@@ -41,24 +41,23 @@ def serve_path(test_paths):
     return test_paths["/"]["tests_path"]
 
 
 class TestEnvironmentError(Exception):
     pass
 
 
 class TestEnvironment(object):
-    def __init__(self, test_paths, testharness_timeout_multipler, pause_after_test, debug_info, options, ssl_config, env_extras):
+    def __init__(self, test_paths, pause_after_test, debug_info, options, ssl_config, env_extras):
         """Context manager that owns the test environment i.e. the http and
         websockets servers"""
         self.test_paths = test_paths
         self.server = None
         self.config_ctx = None
         self.config = None
-        self.testharness_timeout_multipler = testharness_timeout_multipler
         self.pause_after_test = pause_after_test
         self.test_server_port = options.pop("test_server_port", True)
         self.debug_info = debug_info
         self.options = options if options is not None else {}
 
         self.cache_manager = multiprocessing.Manager()
         self.stash = serve.stash.StashServer()
         self.env_extras = env_extras
@@ -165,20 +164,17 @@ class TestEnvironment(object):
             pass
 
     def get_routes(self):
         route_builder = serve.RoutesBuilder()
 
         for path, format_args, content_type, route in [
                 ("testharness_runner.html", {}, "text/html", "/testharness_runner.html"),
                 (self.options.get("testharnessreport", "testharnessreport.js"),
-                 {"output": self.pause_after_test,
-                  "timeout_multiplier": self.testharness_timeout_multipler,
-                  "explicit_timeout": "true" if self.debug_info is not None else "false"},
-                 "text/javascript;charset=utf8",
+                 {"output": self.pause_after_test}, "text/javascript;charset=utf8",
                  "/resources/testharnessreport.js")]:
             path = os.path.normpath(os.path.join(here, path))
             # Note that .headers. files don't apply to static routes, so we need to
             # readd any static headers here.
             headers = {"Cache-Control": "max-age=3600"}
             route_builder.add_static(path, format_args, content_type, route,
                                      headers=headers)
 
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/base.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/base.py
@@ -517,17 +517,17 @@ class CallbackHandler(object):
         self.logger.debug("Got async callback: %s" % result[1])
         try:
             callback = self.callbacks[command]
         except KeyError:
             raise ValueError("Unknown callback type %r" % result[1])
         return callback(url, payload)
 
     def process_complete(self, url, payload):
-        rv = [strip_server(url)] + payload
+        rv = [url] + payload
         return True, rv
 
     def process_action(self, url, payload):
         action = payload["action"]
         self.logger.debug("Got action: %s" % action)
         try:
             action_handler = self.actions[action]
         except KeyError:
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executormarionette.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executormarionette.py
@@ -1,12 +1,11 @@
 import json
 import os
 import threading
-import time
 import traceback
 import urlparse
 import uuid
 
 errors = None
 marionette = None
 pytestrunner = None
 
@@ -182,53 +181,39 @@ class MarionetteTestharnessProtocolPart(
                 alert = self.marionette.switch_to_alert()
                 try:
                     alert.dismiss()
                 except errors.NoAlertPresentException:
                     pass
             else:
                 break
 
-    def get_test_window(self, window_id, parent, timeout=5):
-        """Find the test window amongst all the open windows.
-        This is assumed to be either the named window or the one after the parent in the list of
-        window handles
-
-        :param window_id: The DOM name of the Window
-        :param parent: The handle of the runner window
-        :param timeout: The time in seconds to wait for the window to appear. This is because in
-                        some implementations there's a race between calling window.open and the
-                        window being added to the list of WebDriver accessible windows."""
+    def get_test_window(self, window_id, parent):
         test_window = None
-        end_time = time.time() + timeout
-        while time.time() < end_time:
-            if window_id:
-                try:
-                    # Try this, it's in Level 1 but nothing supports it yet
-                    win_s = self.parent.base.execute_script("return window['%s'];" % self.window_id)
-                    win_obj = json.loads(win_s)
-                    test_window = win_obj["window-fcc6-11e5-b4f8-330a88ab9d7f"]
-                except Exception:
-                    pass
+        if window_id:
+            try:
+                # Try this, it's in Level 1 but nothing supports it yet
+                win_s = self.parent.base.execute_script("return window['%s'];" % self.window_id)
+                win_obj = json.loads(win_s)
+                test_window = win_obj["window-fcc6-11e5-b4f8-330a88ab9d7f"]
+            except Exception:
+                pass
 
-            if test_window is None:
-                handles = self.marionette.window_handles
-                if len(handles) == 2:
-                    test_window = next(iter(set(handles) - set([parent])))
-                elif handles[0] == parent and len(handles) > 2:
-                    # Hope the first one here is the test window
-                    test_window = handles[1]
+        if test_window is None:
+            after = self.marionette.window_handles
+            if len(after) == 2:
+                test_window = next(iter(set(after) - set([parent])))
+            elif after[0] == parent and len(after) > 2:
+                # Hope the first one here is the test window
+                test_window = after[1]
+            else:
+                raise Exception("unable to find test window")
 
-            if test_window is not None:
-                assert test_window != parent
-                return test_window
-
-            time.sleep(0.1)
-
-        raise Exception("unable to find test window")
+        assert test_window != parent
+        return test_window
 
 
 class MarionettePrefsProtocolPart(PrefsProtocolPart):
     def setup(self):
         self.marionette = self.parent.marionette
 
     def set(self, name, value):
         if value.lower() not in ("true", "false"):
@@ -635,18 +620,18 @@ class MarionetteTestharnessExecutor(Test
                                      timeout_multiplier=timeout_multiplier,
                                      debug_info=debug_info)
         self.protocol = MarionetteProtocol(self,
                                            browser,
                                            capabilities,
                                            timeout_multiplier,
                                            kwargs["e10s"],
                                            ccov)
-        with open(os.path.join(here, "testharness_webdriver_resume.js")) as f:
-            self.script_resume = f.read()
+        self.script = open(os.path.join(here, "testharness_webdriver.js")).read()
+        self.script_resume = open(os.path.join(here, "testharness_webdriver_resume.js")).read()
         self.close_after_done = close_after_done
         self.window_id = str(uuid.uuid4())
         self.debug = debug
 
         self.original_pref_values = {}
 
         if marionette is None:
             do_delayed_imports()
@@ -689,28 +674,39 @@ class MarionetteTestharnessExecutor(Test
         if success:
             return self.convert_result(test, data, extra=extra)
 
         return (test.result_cls(extra=extra, *data), [])
 
     def do_testharness(self, protocol, url, timeout):
         parent_window = protocol.testharness.close_old_windows(protocol)
 
+        if timeout is not None:
+            timeout_ms = str(timeout * 1000)
+        else:
+            timeout_ms = "null"
+
         if self.protocol.coverage.is_enabled:
             self.protocol.coverage.reset()
 
-        format_map = {"url": strip_server(url)}
+        format_map = {"abs_url": url,
+                      "url": strip_server(url),
+                      "window_id": self.window_id,
+                      "timeout_multiplier": self.timeout_multiplier,
+                      "timeout": timeout_ms,
+                      "explicit_timeout": timeout is None}
 
-        protocol.base.execute_script("window.open('about:blank', '%s', 'noopener')" % self.window_id)
-        test_window = protocol.testharness.get_test_window(self.window_id, parent_window,
-                                                           timeout=10*self.timeout_multiplier)
-        self.protocol.base.set_window(test_window)
+        script = self.script % format_map
+
+        protocol.base.execute_script(script, async=True)
+        test_window = protocol.testharness.get_test_window(self.window_id, parent_window)
+
         handler = CallbackHandler(self.logger, protocol, test_window)
-        protocol.marionette.navigate(url)
         while True:
+            self.protocol.base.set_window(test_window)
             result = protocol.base.execute_script(
                 self.script_resume % format_map, async=True)
             if result is None:
                 # This can happen if we get an content process crash
                 return None
             done, rv = handler(result)
             if done:
                 break
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executormarionette.py.orig
@@ -0,0 +1,886 @@
+import json
+import os
+import threading
+import time
+import traceback
+import urlparse
+import uuid
+
+errors = None
+marionette = None
+pytestrunner = None
+
+here = os.path.join(os.path.split(__file__)[0])
+
+from .base import (CallbackHandler,
+                   RefTestExecutor,
+                   RefTestImplementation,
+                   TestharnessExecutor,
+                   WdspecExecutor,
+                   WebDriverProtocol,
+                   extra_timeout,
+                   strip_server)
+from .protocol import (ActionSequenceProtocolPart,
+                       AssertsProtocolPart,
+                       BaseProtocolPart,
+                       TestharnessProtocolPart,
+                       PrefsProtocolPart,
+                       Protocol,
+                       StorageProtocolPart,
+                       SelectorProtocolPart,
+                       ClickProtocolPart,
+                       SendKeysProtocolPart,
+                       TestDriverProtocolPart,
+                       CoverageProtocolPart)
+from ..testrunner import Stop
+from ..webdriver_server import GeckoDriverServer
+
+
+def do_delayed_imports():
+    global errors, marionette
+
+    # Marionette client used to be called marionette, recently it changed
+    # to marionette_driver for unfathomable reasons
+    try:
+        import marionette
+        from marionette import errors
+    except ImportError:
+        from marionette_driver import marionette, errors
+
+
+class MarionetteBaseProtocolPart(BaseProtocolPart):
+    def __init__(self, parent):
+        super(MarionetteBaseProtocolPart, self).__init__(parent)
+        self.timeout = None
+
+    def setup(self):
+        self.marionette = self.parent.marionette
+
+    def execute_script(self, script, async=False):
+        method = self.marionette.execute_async_script if async else self.marionette.execute_script
+        return method(script, new_sandbox=False, sandbox=None)
+
+    def set_timeout(self, timeout):
+        """Set the Marionette script timeout.
+
+        :param timeout: Script timeout in seconds
+
+        """
+        if timeout != self.timeout:
+            self.marionette.timeout.script = timeout
+            self.timeout = timeout
+
+    @property
+    def current_window(self):
+        return self.marionette.current_window_handle
+
+    def set_window(self, handle):
+        self.marionette.switch_to_window(handle)
+
+    def wait(self):
+        try:
+            socket_timeout = self.marionette.client.socket_timeout
+        except AttributeError:
+            # This can happen if there was a crash
+            return
+        if socket_timeout:
+            try:
+                self.marionette.timeout.script = socket_timeout / 2
+            except IOError:
+                self.logger.debug("Socket closed")
+                return
+
+        while True:
+            try:
+                self.marionette.execute_async_script("")
+            except errors.NoSuchWindowException:
+                # The window closed
+                break
+            except errors.ScriptTimeoutException:
+                self.logger.debug("Script timed out")
+                pass
+            except errors.JavascriptException as e:
+                # This can happen if we navigate, but just keep going
+                self.logger.debug(e.message)
+                pass
+            except IOError:
+                self.logger.debug("Socket closed")
+                break
+            except Exception as e:
+                self.logger.warning(traceback.format_exc(e))
+                break
+
+
+class MarionetteTestharnessProtocolPart(TestharnessProtocolPart):
+    def __init__(self, parent):
+        super(MarionetteTestharnessProtocolPart, self).__init__(parent)
+        self.runner_handle = None
+        with open(os.path.join(here, "runner.js")) as f:
+            self.runner_script = f.read()
+
+    def setup(self):
+        self.marionette = self.parent.marionette
+
+    def load_runner(self, url_protocol):
+        # Check if we previously had a test window open, and if we did make sure it's closed
+        if self.runner_handle:
+            self._close_windows()
+        url = urlparse.urljoin(self.parent.executor.server_url(url_protocol),
+                               "/testharness_runner.html")
+        self.logger.debug("Loading %s" % url)
+        try:
+            self.dismiss_alert(lambda: self.marionette.navigate(url))
+        except Exception as e:
+            self.logger.critical(
+                "Loading initial page %s failed. Ensure that the "
+                "there are no other programs bound to this port and "
+                "that your firewall rules or network setup does not "
+                "prevent access.\e%s" % (url, traceback.format_exc(e)))
+            raise
+        self.runner_handle = self.marionette.current_window_handle
+        format_map = {"title": threading.current_thread().name.replace("'", '"')}
+        self.parent.base.execute_script(self.runner_script % format_map)
+
+    def _close_windows(self):
+        handles = self.marionette.window_handles
+        runner_handle = None
+        try:
+            handles.remove(self.runner_handle)
+            runner_handle = self.runner_handle
+        except ValueError:
+            # The runner window probably changed id but we can restore it
+            # This isn't supposed to happen, but marionette ids are not yet stable
+            # We assume that the first handle returned corresponds to the runner,
+            # but it hopefully doesn't matter too much if that assumption is
+            # wrong since we reload the runner in that tab anyway.
+            runner_handle = handles.pop(0)
+            self.logger.info("Changing harness_window to %s" % runner_handle)
+
+        for handle in handles:
+            try:
+                self.dismiss_alert(lambda: self.marionette.switch_to_window(handle))
+                self.marionette.switch_to_window(handle)
+                self.logger.info("Closing window %s" % handle)
+                self.marionette.close()
+            except errors.NoSuchWindowException:
+                # We might have raced with the previous test to close this
+                # window, skip it.
+                pass
+        self.marionette.switch_to_window(runner_handle)
+        return runner_handle
+
+    def close_old_windows(self, url_protocol):
+        runner_handle = self._close_windows()
+        if runner_handle != self.runner_handle:
+            self.load_runner(url_protocol)
+        return self.runner_handle
+
+    def dismiss_alert(self, f):
+        while True:
+            try:
+                f()
+            except errors.UnexpectedAlertOpen:
+                alert = self.marionette.switch_to_alert()
+                try:
+                    alert.dismiss()
+                except errors.NoAlertPresentException:
+                    pass
+            else:
+                break
+
+    def get_test_window(self, window_id, parent, timeout=5):
+        """Find the test window amongst all the open windows.
+        This is assumed to be either the named window or the one after the parent in the list of
+        window handles
+
+        :param window_id: The DOM name of the Window
+        :param parent: The handle of the runner window
+        :param timeout: The time in seconds to wait for the window to appear. This is because in
+                        some implementations there's a race between calling window.open and the
+                        window being added to the list of WebDriver accessible windows."""
+        test_window = None
+        end_time = time.time() + timeout
+        while time.time() < end_time:
+            if window_id:
+                try:
+                    # Try this, it's in Level 1 but nothing supports it yet
+                    win_s = self.parent.base.execute_script("return window['%s'];" % self.window_id)
+                    win_obj = json.loads(win_s)
+                    test_window = win_obj["window-fcc6-11e5-b4f8-330a88ab9d7f"]
+                except Exception:
+                    pass
+
+            if test_window is None:
+                handles = self.marionette.window_handles
+                if len(handles) == 2:
+                    test_window = next(iter(set(handles) - set([parent])))
+                elif handles[0] == parent and len(handles) > 2:
+                    # Hope the first one here is the test window
+                    test_window = handles[1]
+
+            if test_window is not None:
+                assert test_window != parent
+                return test_window
+
+            time.sleep(0.1)
+
+        raise Exception("unable to find test window")
+
+
+class MarionettePrefsProtocolPart(PrefsProtocolPart):
+    def setup(self):
+        self.marionette = self.parent.marionette
+
+    def set(self, name, value):
+        if value.lower() not in ("true", "false"):
+            try:
+                int(value)
+            except ValueError:
+                value = "'%s'" % value
+        else:
+            value = value.lower()
+
+        self.logger.info("Setting pref %s (%s)" % (name, value))
+
+        script = """
+            let prefInterface = Components.classes["@mozilla.org/preferences-service;1"]
+                                          .getService(Components.interfaces.nsIPrefBranch);
+            let pref = '%s';
+            let type = prefInterface.getPrefType(pref);
+            let value = %s;
+            switch(type) {
+                case prefInterface.PREF_STRING:
+                    prefInterface.setCharPref(pref, value);
+                    break;
+                case prefInterface.PREF_BOOL:
+                    prefInterface.setBoolPref(pref, value);
+                    break;
+                case prefInterface.PREF_INT:
+                    prefInterface.setIntPref(pref, value);
+                    break;
+            }
+            """ % (name, value)
+        with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+            self.marionette.execute_script(script)
+
+    def clear(self, name):
+        self.logger.info("Clearing pref %s" % (name))
+        script = """
+            let prefInterface = Components.classes["@mozilla.org/preferences-service;1"]
+                                          .getService(Components.interfaces.nsIPrefBranch);
+            let pref = '%s';
+            prefInterface.clearUserPref(pref);
+            """ % name
+        with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+            self.marionette.execute_script(script)
+
+    def get(self, name):
+        script = """
+            let prefInterface = Components.classes["@mozilla.org/preferences-service;1"]
+                                          .getService(Components.interfaces.nsIPrefBranch);
+            let pref = '%s';
+            let type = prefInterface.getPrefType(pref);
+            switch(type) {
+                case prefInterface.PREF_STRING:
+                    return prefInterface.getCharPref(pref);
+                case prefInterface.PREF_BOOL:
+                    return prefInterface.getBoolPref(pref);
+                case prefInterface.PREF_INT:
+                    return prefInterface.getIntPref(pref);
+                case prefInterface.PREF_INVALID:
+                    return null;
+            }
+            """ % name
+        with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+            self.marionette.execute_script(script)
+
+
+class MarionetteStorageProtocolPart(StorageProtocolPart):
+    def setup(self):
+        self.marionette = self.parent.marionette
+
+    def clear_origin(self, url):
+        self.logger.info("Clearing origin %s" % (url))
+        script = """
+            let url = '%s';
+            let uri = Components.classes["@mozilla.org/network/io-service;1"]
+                                .getService(Ci.nsIIOService)
+                                .newURI(url);
+            let ssm = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
+                                .getService(Ci.nsIScriptSecurityManager);
+            let principal = ssm.createCodebasePrincipal(uri, {});
+            let qms = Components.classes["@mozilla.org/dom/quota-manager-service;1"]
+                                .getService(Components.interfaces.nsIQuotaManagerService);
+            qms.clearStoragesForPrincipal(principal, "default", null, true);
+            """ % url
+        with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+            self.marionette.execute_script(script)
+
+
+class MarionetteAssertsProtocolPart(AssertsProtocolPart):
+    def setup(self):
+        self.assert_count = {"chrome": 0, "content": 0}
+        self.chrome_assert_count = 0
+        self.marionette = self.parent.marionette
+
+    def get(self):
+        script = """
+        debug = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2);
+        if (debug.isDebugBuild) {
+          return debug.assertionCount;
+        }
+        return 0;
+        """
+
+        def get_count(context, **kwargs):
+            try:
+                context_count = self.marionette.execute_script(script, **kwargs)
+                if context_count:
+                    self.parent.logger.info("Got %s assert count %s" % (context, context_count))
+                    test_count = context_count - self.assert_count[context]
+                    self.assert_count[context] = context_count
+                    return test_count
+            except errors.NoSuchWindowException:
+                # If the window was already closed
+                self.parent.logger.warning("Failed to get assertion count; window was closed")
+            except (errors.MarionetteException, IOError):
+                # This usually happens if the process crashed
+                pass
+
+        counts = []
+        with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+            counts.append(get_count("chrome"))
+        if self.parent.e10s:
+            counts.append(get_count("content", sandbox="system"))
+
+        counts = [item for item in counts if item is not None]
+
+        if not counts:
+            return None
+
+        return sum(counts)
+
+
+class MarionetteSelectorProtocolPart(SelectorProtocolPart):
+    def setup(self):
+        self.marionette = self.parent.marionette
+
+    def elements_by_selector(self, selector):
+        return self.marionette.find_elements("css selector", selector)
+
+
+class MarionetteClickProtocolPart(ClickProtocolPart):
+    def setup(self):
+        self.marionette = self.parent.marionette
+
+    def element(self, element):
+        return element.click()
+
+
+class MarionetteSendKeysProtocolPart(SendKeysProtocolPart):
+    def setup(self):
+        self.marionette = self.parent.marionette
+
+    def send_keys(self, element, keys):
+        return element.send_keys(keys)
+
+
+class MarionetteActionSequenceProtocolPart(ActionSequenceProtocolPart):
+    def setup(self):
+        self.marionette = self.parent.marionette
+
+    def send_actions(self, actions):
+        actions = self.marionette._to_json(actions)
+        self.logger.info(actions)
+        self.marionette._send_message("WebDriver:PerformActions", actions)
+
+
+class MarionetteTestDriverProtocolPart(TestDriverProtocolPart):
+    def setup(self):
+        self.marionette = self.parent.marionette
+
+    def send_message(self, message_type, status, message=None):
+        obj = {
+            "type": "testdriver-%s" % str(message_type),
+            "status": str(status)
+        }
+        if message:
+            obj["message"] = str(message)
+        self.parent.base.execute_script("window.postMessage(%s, '*')" % json.dumps(obj))
+
+
+class MarionetteCoverageProtocolPart(CoverageProtocolPart):
+    def setup(self):
+        self.marionette = self.parent.marionette
+
+        if not self.parent.ccov:
+            self.is_enabled = False
+            return
+
+        script = """
+            ChromeUtils.import("chrome://marionette/content/PerTestCoverageUtils.jsm");
+            return PerTestCoverageUtils.enabled;
+            """
+        with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+            self.is_enabled = self.marionette.execute_script(script)
+
+    def reset(self):
+        script = """
+            var callback = arguments[arguments.length - 1];
+
+            ChromeUtils.import("chrome://marionette/content/PerTestCoverageUtils.jsm");
+            PerTestCoverageUtils.beforeTest().then(callback, callback);
+            """
+        with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+            try:
+                error = self.marionette.execute_async_script(script)
+                if error is not None:
+                    raise Exception('Failure while resetting counters: %s' % json.dumps(error))
+            except (errors.MarionetteException, IOError):
+                # This usually happens if the process crashed
+                pass
+
+    def dump(self):
+        if len(self.marionette.window_handles):
+            handle = self.marionette.window_handles[0]
+            self.marionette.switch_to_window(handle)
+
+        script = """
+            var callback = arguments[arguments.length - 1];
+
+            ChromeUtils.import("chrome://marionette/content/PerTestCoverageUtils.jsm");
+            PerTestCoverageUtils.afterTest().then(callback, callback);
+            """
+        with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+            try:
+                error = self.marionette.execute_async_script(script)
+                if error is not None:
+                    raise Exception('Failure while dumping counters: %s' % json.dumps(error))
+            except (errors.MarionetteException, IOError):
+                # This usually happens if the process crashed
+                pass
+
+
+class MarionetteProtocol(Protocol):
+    implements = [MarionetteBaseProtocolPart,
+                  MarionetteTestharnessProtocolPart,
+                  MarionettePrefsProtocolPart,
+                  MarionetteStorageProtocolPart,
+                  MarionetteSelectorProtocolPart,
+                  MarionetteClickProtocolPart,
+                  MarionetteSendKeysProtocolPart,
+                  MarionetteActionSequenceProtocolPart,
+                  MarionetteTestDriverProtocolPart,
+                  MarionetteAssertsProtocolPart,
+                  MarionetteCoverageProtocolPart]
+
+    def __init__(self, executor, browser, capabilities=None, timeout_multiplier=1, e10s=True, ccov=False):
+        do_delayed_imports()
+
+        super(MarionetteProtocol, self).__init__(executor, browser)
+        self.marionette = None
+        self.marionette_port = browser.marionette_port
+        self.capabilities = capabilities
+        self.timeout_multiplier = timeout_multiplier
+        self.runner_handle = None
+        self.e10s = e10s
+        self.ccov = ccov
+
+    def connect(self):
+        self.logger.debug("Connecting to Marionette on port %i" % self.marionette_port)
+        startup_timeout = marionette.Marionette.DEFAULT_STARTUP_TIMEOUT * self.timeout_multiplier
+        self.marionette = marionette.Marionette(host='127.0.0.1',
+                                                port=self.marionette_port,
+                                                socket_timeout=None,
+                                                startup_timeout=startup_timeout)
+
+        self.logger.debug("Waiting for Marionette connection")
+        while True:
+            try:
+                self.marionette.raise_for_port()
+                break
+            except IOError:
+                # When running in a debugger wait indefinitely for Firefox to start
+                if self.executor.debug_info is None:
+                    raise
+
+        self.logger.debug("Starting Marionette session")
+        self.marionette.start_session(self.capabilities)
+        self.logger.debug("Marionette session started")
+
+    def after_connect(self):
+        pass
+
+    def teardown(self):
+        try:
+            self.marionette._request_in_app_shutdown()
+            self.marionette.delete_session(send_request=False)
+        except Exception:
+            # This is typically because the session never started
+            pass
+        if self.marionette is not None:
+            del self.marionette
+        super(MarionetteProtocol, self).teardown()
+
+    @property
+    def is_alive(self):
+        try:
+            self.marionette.current_window_handle
+        except Exception:
+            return False
+        return True
+
+    def on_environment_change(self, old_environment, new_environment):
+        #Unset all the old prefs
+        for name in old_environment.get("prefs", {}).iterkeys():
+            value = self.executor.original_pref_values[name]
+            if value is None:
+                self.prefs.clear(name)
+            else:
+                self.prefs.set(name, value)
+
+        for name, value in new_environment.get("prefs", {}).iteritems():
+            self.executor.original_pref_values[name] = self.prefs.get(name)
+            self.prefs.set(name, value)
+
+
+class ExecuteAsyncScriptRun(object):
+    def __init__(self, logger, func, protocol, url, timeout):
+        self.logger = logger
+        self.result = (None, None)
+        self.protocol = protocol
+        self.func = func
+        self.url = url
+        self.timeout = timeout
+        self.result_flag = threading.Event()
+
+    def run(self):
+        index = self.url.rfind("/storage/")
+        if index != -1:
+            # Clear storage
+            self.protocol.storage.clear_origin(self.url)
+
+        timeout = self.timeout
+
+        try:
+            if timeout is not None:
+                self.protocol.base.set_timeout(timeout + extra_timeout)
+            else:
+                # We just want it to never time out, really, but marionette doesn't
+                # make that possible. It also seems to time out immediately if the
+                # timeout is set too high. This works at least.
+                self.protocol.base.set_timeout(2**28 - 1)
+        except IOError:
+            self.logger.error("Lost marionette connection before starting test")
+            return Stop
+
+        if timeout is not None:
+            wait_timeout = timeout + 2 * extra_timeout
+        else:
+            wait_timeout = None
+
+        timer = threading.Timer(wait_timeout, self._timeout)
+        timer.start()
+
+        self._run()
+
+        self.result_flag.wait()
+        timer.cancel()
+
+        if self.result == (None, None):
+            self.logger.debug("Timed out waiting for a result")
+            self.result = False, ("EXTERNAL-TIMEOUT", None)
+        elif self.result[1] is None:
+            # We didn't get any data back from the test, so check if the
+            # browser is still responsive
+            if self.protocol.is_alive:
+                self.result = False, ("INTERNAL-ERROR", None)
+            else:
+                self.result = False, ("CRASH", None)
+        return self.result
+
+    def _run(self):
+        try:
+            self.result = True, self.func(self.protocol, self.url, self.timeout)
+        except errors.ScriptTimeoutException:
+            self.logger.debug("Got a marionette timeout")
+            self.result = False, ("EXTERNAL-TIMEOUT", None)
+        except IOError:
+            # This can happen on a crash
+            # Also, should check after the test if the firefox process is still running
+            # and otherwise ignore any other result and set it to crash
+            self.result = False, ("CRASH", None)
+        except Exception as e:
+            message = getattr(e, "message", "")
+            if message:
+                message += "\n"
+            message += traceback.format_exc(e)
+            self.logger.warning(traceback.format_exc())
+            self.result = False, ("INTERNAL-ERROR", e)
+        finally:
+            self.result_flag.set()
+
+    def _timeout(self):
+        self.result = False, ("EXTERNAL-TIMEOUT", None)
+        self.result_flag.set()
+
+
+class MarionetteTestharnessExecutor(TestharnessExecutor):
+    supports_testdriver = True
+
+    def __init__(self, browser, server_config, timeout_multiplier=1,
+                 close_after_done=True, debug_info=None, capabilities=None,
+                 debug=False, ccov=False, **kwargs):
+        """Marionette-based executor for testharness.js tests"""
+        TestharnessExecutor.__init__(self, browser, server_config,
+                                     timeout_multiplier=timeout_multiplier,
+                                     debug_info=debug_info)
+        self.protocol = MarionetteProtocol(self,
+                                           browser,
+                                           capabilities,
+                                           timeout_multiplier,
+                                           kwargs["e10s"],
+                                           ccov)
+        with open(os.path.join(here, "testharness_webdriver_resume.js")) as f:
+            self.script_resume = f.read()
+        self.close_after_done = close_after_done
+        self.window_id = str(uuid.uuid4())
+        self.debug = debug
+
+        self.original_pref_values = {}
+
+        if marionette is None:
+            do_delayed_imports()
+
+    def setup(self, runner):
+        super(MarionetteTestharnessExecutor, self).setup(runner)
+        self.protocol.testharness.load_runner(self.last_environment["protocol"])
+
+    def is_alive(self):
+        return self.protocol.is_alive
+
+    def on_environment_change(self, new_environment):
+        self.protocol.on_environment_change(self.last_environment, new_environment)
+
+        if new_environment["protocol"] != self.last_environment["protocol"]:
+            self.protocol.testharness.load_runner(new_environment["protocol"])
+
+    def do_test(self, test):
+        timeout = (test.timeout * self.timeout_multiplier if self.debug_info is None
+                   else None)
+
+        success, data = ExecuteAsyncScriptRun(self.logger,
+                                              self.do_testharness,
+                                              self.protocol,
+                                              self.test_url(test),
+                                              timeout).run()
+        # The format of data depends on whether the test ran to completion or not
+        # For asserts we only care about the fact that if it didn't complete, the
+        # status is in the first field.
+        status = None
+        if not success:
+            status = data[0]
+
+        extra = None
+        if self.debug and (success or status not in ("CRASH", "INTERNAL-ERROR")):
+            assertion_count = self.protocol.asserts.get()
+            if assertion_count is not None:
+                extra = {"assertion_count": assertion_count}
+
+        if success:
+            return self.convert_result(test, data, extra=extra)
+
+        return (test.result_cls(extra=extra, *data), [])
+
+    def do_testharness(self, protocol, url, timeout):
+        parent_window = protocol.testharness.close_old_windows(protocol)
+
+        if self.protocol.coverage.is_enabled:
+            self.protocol.coverage.reset()
+
+        format_map = {"url": strip_server(url)}
+
+        protocol.base.execute_script("window.open('about:blank', '%s', 'noopener')" % self.window_id)
+        test_window = protocol.testharness.get_test_window(self.window_id, parent_window,
+                                                           timeout=10*self.timeout_multiplier)
+        self.protocol.base.set_window(test_window)
+        handler = CallbackHandler(self.logger, protocol, test_window)
+        protocol.marionette.navigate(url)
+        while True:
+            result = protocol.base.execute_script(
+                self.script_resume % format_map, async=True)
+            if result is None:
+                # This can happen if we get an content process crash
+                return None
+            done, rv = handler(result)
+            if done:
+                break
+
+        if self.protocol.coverage.is_enabled:
+            self.protocol.coverage.dump()
+
+        return rv
+
+
+class MarionetteRefTestExecutor(RefTestExecutor):
+    def __init__(self, browser, server_config, timeout_multiplier=1,
+                 screenshot_cache=None, close_after_done=True,
+                 debug_info=None, reftest_internal=False,
+                 reftest_screenshot="unexpected", ccov=False,
+                 group_metadata=None, capabilities=None, debug=False, **kwargs):
+        """Marionette-based executor for reftests"""
+        RefTestExecutor.__init__(self,
+                                 browser,
+                                 server_config,
+                                 screenshot_cache=screenshot_cache,
+                                 timeout_multiplier=timeout_multiplier,
+                                 debug_info=debug_info)
+        self.protocol = MarionetteProtocol(self, browser, capabilities,
+                                           timeout_multiplier, kwargs["e10s"],
+                                           ccov)
+        self.implementation = (InternalRefTestImplementation
+                               if reftest_internal
+                               else RefTestImplementation)(self)
+        self.implementation_kwargs = ({"screenshot": reftest_screenshot} if
+                                      reftest_internal else {})
+
+        self.close_after_done = close_after_done
+        self.has_window = False
+        self.original_pref_values = {}
+        self.group_metadata = group_metadata
+        self.debug = debug
+
+        with open(os.path.join(here, "reftest.js")) as f:
+            self.script = f.read()
+        with open(os.path.join(here, "reftest-wait_marionette.js")) as f:
+            self.wait_script = f.read()
+
+    def setup(self, runner):
+        super(self.__class__, self).setup(runner)
+        self.implementation.setup(**self.implementation_kwargs)
+
+    def teardown(self):
+        try:
+            self.implementation.teardown()
+            handles = self.protocol.marionette.window_handles
+            if handles:
+                self.protocol.marionette.switch_to_window(handles[0])
+            super(self.__class__, self).teardown()
+        except Exception as e:
+            # Ignore errors during teardown
+            self.logger.warning("Exception during reftest teardown:\n%s" %
+                                traceback.format_exc(e))
+
+    def is_alive(self):
+        return self.protocol.is_alive
+
+    def on_environment_change(self, new_environment):
+        self.protocol.on_environment_change(self.last_environment, new_environment)
+
+    def do_test(self, test):
+        if not isinstance(self.implementation, InternalRefTestImplementation):
+            if self.close_after_done and self.has_window:
+                self.protocol.marionette.close()
+                self.protocol.marionette.switch_to_window(
+                    self.protocol.marionette.window_handles[-1])
+                self.has_window = False
+
+            if not self.has_window:
+                self.protocol.base.execute_script(self.script)
+                self.protocol.base.set_window(self.protocol.marionette.window_handles[-1])
+                self.has_window = True
+
+        if self.protocol.coverage.is_enabled:
+            self.protocol.coverage.reset()
+
+        result = self.implementation.run_test(test)
+
+        if self.protocol.coverage.is_enabled:
+            self.protocol.coverage.dump()
+
+        if self.debug:
+            assertion_count = self.protocol.asserts.get()
+            if "extra" not in result:
+                result["extra"] = {}
+            result["extra"]["assertion_count"] = assertion_count
+
+        return self.convert_result(test, result)
+
+    def screenshot(self, test, viewport_size, dpi):
+        # https://github.com/w3c/wptrunner/issues/166
+        assert viewport_size is None
+        assert dpi is None
+
+        timeout = self.timeout_multiplier * test.timeout if self.debug_info is None else None
+
+        test_url = self.test_url(test)
+
+        return ExecuteAsyncScriptRun(self.logger,
+                                     self._screenshot,
+                                     self.protocol,
+                                     test_url,
+                                     timeout).run()
+
+    def _screenshot(self, protocol, url, timeout):
+        protocol.marionette.navigate(url)
+
+        protocol.base.execute_script(self.wait_script, async=True)
+
+        screenshot = protocol.marionette.screenshot(full=False)
+        # strip off the data:img/png, part of the url
+        if screenshot.startswith("data:image/png;base64,"):
+            screenshot = screenshot.split(",", 1)[1]
+
+        return screenshot
+
+
+class InternalRefTestImplementation(object):
+    def __init__(self, executor):
+        self.timeout_multiplier = executor.timeout_multiplier
+        self.executor = executor
+
+    @property
+    def logger(self):
+        return self.executor.logger
+
+    def setup(self, screenshot="unexpected"):
+        data = {"screenshot": screenshot}
+        if self.executor.group_metadata is not None:
+            data["urlCount"] = {urlparse.urljoin(self.executor.server_url(key[0]), key[1]):value
+                                for key, value in self.executor.group_metadata.get("url_count", {}).iteritems()
+                                if value > 1}
+        self.executor.protocol.marionette.set_context(self.executor.protocol.marionette.CONTEXT_CHROME)
+        self.executor.protocol.marionette._send_message("reftest:setup", data)
+
+    def run_test(self, test):
+        references = self.get_references(test)
+        timeout = (test.timeout * 1000) * self.timeout_multiplier
+        rv = self.executor.protocol.marionette._send_message("reftest:run",
+                                                             {"test": self.executor.test_url(test),
+                                                              "references": references,
+                                                              "expected": test.expected(),
+                                                              "timeout": timeout})["value"]
+        return rv
+
+    def get_references(self, node):
+        rv = []
+        for item, relation in node.references:
+            rv.append([self.executor.test_url(item), self.get_references(item), relation])
+        return rv
+
+    def teardown(self):
+        try:
+            self.executor.protocol.marionette._send_message("reftest:teardown", {})
+            self.executor.protocol.marionette.set_context(self.executor.protocol.marionette.CONTEXT_CONTENT)
+        except Exception as e:
+            # Ignore errors during teardown
+            self.logger.warning(traceback.format_exc(e))
+
+
+
+class GeckoDriverProtocol(WebDriverProtocol):
+    server_cls = GeckoDriverServer
+
+
+class MarionetteWdspecExecutor(WdspecExecutor):
+    protocol_cls = GeckoDriverProtocol
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executorselenium.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executorselenium.py
@@ -1,13 +1,12 @@
 import json
 import os
 import socket
 import threading
-import time
 import traceback
 import urlparse
 import uuid
 
 from .base import (CallbackHandler,
                    RefTestExecutor,
                    RefTestImplementation,
                    TestharnessExecutor,
@@ -98,53 +97,39 @@ class SeleniumTestharnessProtocolPart(Te
             try:
                 self.webdriver.switch_to_window(handle)
                 self.webdriver.close()
             except exceptions.NoSuchWindowException:
                 pass
         self.webdriver.switch_to_window(self.runner_handle)
         return self.runner_handle
 
-    def get_test_window(self, window_id, parent, timeout=5):
-        """Find the test window amongst all the open windows.
-        This is assumed to be either the named window or the one after the parent in the list of
-        window handles
-
-        :param window_id: The DOM name of the Window
-        :param parent: The handle of the runner window
-        :param timeout: The time in seconds to wait for the window to appear. This is because in
-                        some implementations there's a race between calling window.open and the
-                        window being added to the list of WebDriver accessible windows."""
+    def get_test_window(self, window_id, parent):
         test_window = None
-        end_time = time.time() + timeout
-        while time.time() < end_time:
-            try:
-                # Try using the JSON serialization of the WindowProxy object,
-                # it's in Level 1 but nothing supports it yet
-                win_s = self.webdriver.execute_script("return window['%s'];" % window_id)
-                win_obj = json.loads(win_s)
-                test_window = win_obj["window-fcc6-11e5-b4f8-330a88ab9d7f"]
-            except Exception:
-                pass
+        try:
+            # Try using the JSON serialization of the WindowProxy object,
+            # it's in Level 1 but nothing supports it yet
+            win_s = self.webdriver.execute_script("return window['%s'];" % window_id)
+            win_obj = json.loads(win_s)
+            test_window = win_obj["window-fcc6-11e5-b4f8-330a88ab9d7f"]
+        except Exception:
+            pass
 
-            if test_window is None:
-                after = self.webdriver.window_handles
-                if len(after) == 2:
-                    test_window = next(iter(set(after) - set([parent])))
-                elif after[0] == parent and len(after) > 2:
-                    # Hope the first one here is the test window
-                    test_window = after[1]
+        if test_window is None:
+            after = self.webdriver.window_handles
+            if len(after) == 2:
+                test_window = next(iter(set(after) - set([parent])))
+            elif after[0] == parent and len(after) > 2:
+                # Hope the first one here is the test window
+                test_window = after[1]
+            else:
+                raise Exception("unable to find test window")
 
-            if test_window is not None:
-                assert test_window != parent
-                return test_window
-
-            time.sleep(0.1)
-
-        raise Exception("unable to find test window")
+        assert test_window != parent
+        return test_window
 
 
 class SeleniumSelectorProtocolPart(SelectorProtocolPart):
     def setup(self):
         self.webdriver = self.parent.webdriver
 
     def elements_by_selector(self, selector):
         return self.webdriver.find_elements_by_css_selector(selector)
@@ -292,16 +277,18 @@ class SeleniumTestharnessExecutor(Testha
     def __init__(self, browser, server_config, timeout_multiplier=1,
                  close_after_done=True, capabilities=None, debug_info=None,
                  **kwargs):
         """Selenium-based executor for testharness.js tests"""
         TestharnessExecutor.__init__(self, browser, server_config,
                                      timeout_multiplier=timeout_multiplier,
                                      debug_info=debug_info)
         self.protocol = SeleniumProtocol(self, browser, capabilities)
+        with open(os.path.join(here, "testharness_webdriver.js")) as f:
+            self.script = f.read()
         with open(os.path.join(here, "testharness_webdriver_resume.js")) as f:
             self.script_resume = f.read()
         self.close_after_done = close_after_done
         self.window_id = str(uuid.uuid4())
 
     def is_alive(self):
         return self.protocol.is_alive()
 
@@ -318,27 +305,30 @@ class SeleniumTestharnessExecutor(Testha
                                     test.timeout * self.timeout_multiplier).run()
 
         if success:
             return self.convert_result(test, data)
 
         return (test.result_cls(*data), [])
 
     def do_testharness(self, protocol, url, timeout):
-        format_map = {"url": strip_server(url)}
+        format_map = {"abs_url": url,
+                      "url": strip_server(url),
+                      "window_id": self.window_id,
+                      "timeout_multiplier": self.timeout_multiplier,
+                      "timeout": timeout * 1000}
 
         parent_window = protocol.testharness.close_old_windows()
         # Now start the test harness
-        protocol.base.execute_script("window.open('about:blank', '%s', 'noopener')" % self.window_id)
-        test_window = protocol.testharness.get_test_window(self.window_id, parent_window,
-                                                           timeout=5*self.timeout_multiplier)
-        self.protocol.base.set_window(test_window)
-        protocol.webdriver.get(url)
+        protocol.base.execute_script(self.script % format_map, async=True)
+        test_window = protocol.testharness.get_test_window(self.window_id, parent_window)
+
         handler = CallbackHandler(self.logger, protocol, test_window)
         while True:
+            self.protocol.base.set_window(test_window)
             result = protocol.base.execute_script(
                 self.script_resume % format_map, async=True)
             done, rv = handler(result)
             if done:
                 break
         return rv
 
 
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executorwebdriver.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/executorwebdriver.py
@@ -1,13 +1,12 @@
 import json
 import os
 import socket
 import threading
-import time
 import traceback
 import urlparse
 import uuid
 
 from .base import (CallbackHandler,
                    RefTestExecutor,
                    RefTestImplementation,
                    TestharnessExecutor,
@@ -90,53 +89,39 @@ class WebDriverTestharnessProtocolPart(T
             try:
                 self.webdriver.window_handle = handle
                 self.webdriver.close()
             except client.NoSuchWindowException:
                 pass
         self.webdriver.window_handle = self.runner_handle
         return self.runner_handle
 
-    def get_test_window(self, window_id, parent, timeout=5):
-        """Find the test window amongst all the open windows.
-        This is assumed to be either the named window or the one after the parent in the list of
-        window handles
-
-        :param window_id: The DOM name of the Window
-        :param parent: The handle of the runner window
-        :param timeout: The time in seconds to wait for the window to appear. This is because in
-                        some implementations there's a race between calling window.open and the
-                        window being added to the list of WebDriver accessible windows."""
+    def get_test_window(self, window_id, parent):
         test_window = None
-        end_time = time.time() + timeout
-        while time.time() < end_time:
-            try:
-                # Try using the JSON serialization of the WindowProxy object,
-                # it's in Level 1 but nothing supports it yet
-                win_s = self.webdriver.execute_script("return window['%s'];" % window_id)
-                win_obj = json.loads(win_s)
-                test_window = win_obj["window-fcc6-11e5-b4f8-330a88ab9d7f"]
-            except Exception:
-                pass
+        try:
+            # Try using the JSON serialization of the WindowProxy object,
+            # it's in Level 1 but nothing supports it yet
+            win_s = self.webdriver.execute_script("return window['%s'];" % window_id)
+            win_obj = json.loads(win_s)
+            test_window = win_obj["window-fcc6-11e5-b4f8-330a88ab9d7f"]
+        except Exception:
+            pass
 
-            if test_window is None:
-                after = self.webdriver.handles
-                if len(after) == 2:
-                    test_window = next(iter(set(after) - set([parent])))
-                elif after[0] == parent and len(after) > 2:
-                    # Hope the first one here is the test window
-                    test_window = after[1]
+        if test_window is None:
+            after = self.webdriver.handles
+            if len(after) == 2:
+                test_window = next(iter(set(after) - set([parent])))
+            elif after[0] == parent and len(after) > 2:
+                # Hope the first one here is the test window
+                test_window = after[1]
+            else:
+                raise Exception("unable to find test window")
 
-            if test_window is not None:
-                assert test_window != parent
-                return test_window
-
-            time.sleep(0.1)
-
-        raise Exception("unable to find test window")
+        assert test_window != parent
+        return test_window
 
 
 class WebDriverSelectorProtocolPart(SelectorProtocolPart):
     def setup(self):
         self.webdriver = self.parent.webdriver
 
     def elements_by_selector(self, selector):
         return self.webdriver.find.css(selector)
@@ -292,27 +277,28 @@ class WebDriverRun(object):
             self.result_flag.set()
 
 
 class WebDriverTestharnessExecutor(TestharnessExecutor):
     supports_testdriver = True
 
     def __init__(self, browser, server_config, timeout_multiplier=1,
                  close_after_done=True, capabilities=None, debug_info=None,
-                 supports_eager_pageload=True, **kwargs):
+                 **kwargs):
         """WebDriver-based executor for testharness.js tests"""
         TestharnessExecutor.__init__(self, browser, server_config,
                                      timeout_multiplier=timeout_multiplier,
                                      debug_info=debug_info)
         self.protocol = WebDriverProtocol(self, browser, capabilities)
+        with open(os.path.join(here, "testharness_webdriver.js")) as f:
+            self.script = f.read()
         with open(os.path.join(here, "testharness_webdriver_resume.js")) as f:
             self.script_resume = f.read()
         self.close_after_done = close_after_done
         self.window_id = str(uuid.uuid4())
-        self.supports_eager_pageload = supports_eager_pageload
 
     def is_alive(self):
         return self.protocol.is_alive()
 
     def on_environment_change(self, new_environment):
         if new_environment["protocol"] != self.last_environment["protocol"]:
             self.protocol.testharness.load_runner(new_environment["protocol"])
 
@@ -325,62 +311,37 @@ class WebDriverTestharnessExecutor(Testh
                                     test.timeout * self.timeout_multiplier).run()
 
         if success:
             return self.convert_result(test, data)
 
         return (test.result_cls(*data), [])
 
     def do_testharness(self, protocol, url, timeout):
-        format_map = {"url": strip_server(url)}
+        format_map = {"abs_url": url,
+                      "url": strip_server(url),
+                      "window_id": self.window_id,
+                      "timeout_multiplier": self.timeout_multiplier,
+                      "timeout": timeout * 1000}
 
         parent_window = protocol.testharness.close_old_windows()
         # Now start the test harness
-        protocol.base.execute_script("window.open('about:blank', '%s', 'noopener')" % self.window_id)
-        test_window = protocol.testharness.get_test_window(self.window_id,
-                                                           parent_window,
-                                                           timeout=5*self.timeout_multiplier)
-        self.protocol.base.set_window(test_window)
+        protocol.base.execute_script(self.script % format_map, async=True)
+        test_window = protocol.testharness.get_test_window(self.window_id, parent_window)
+
         handler = CallbackHandler(self.logger, protocol, test_window)
-        protocol.webdriver.url = url
-
-        if not self.supports_eager_pageload:
-            self.wait_for_load(protocol)
-
         while True:
+            self.protocol.base.set_window(test_window)
             result = protocol.base.execute_script(
                 self.script_resume % format_map, async=True)
             done, rv = handler(result)
             if done:
                 break
         return rv
 
-    def wait_for_load(self, protocol):
-        # pageLoadStrategy=eager doesn't work in Chrome so try to emulate in user script
-        loaded = False
-        seen_error = False
-        while not loaded:
-            try:
-                loaded = protocol.base.execute_script("""
-var callback = arguments[arguments.length - 1];
-if (location.href === "about:blank") {
-  callback(false);
-} else if (document.readyState !== "loading") {
-  callback(true);
-} else {
-  document.addEventListener("readystatechange", () => {if (document.readyState !== "loading") {callback(true)}});
-}""", async=True)
-            except client.JavascriptErrorException:
-                # We can get an error here if the script runs in the initial about:blank
-                # document before it has navigated, with the driver returning an error
-                # indicating that the document was unloaded
-                if seen_error:
-                    raise
-                seen_error = True
-
 
 class WebDriverRefTestExecutor(RefTestExecutor):
     def __init__(self, browser, server_config, timeout_multiplier=1,
                  screenshot_cache=None, close_after_done=True,
                  debug_info=None, capabilities=None, **kwargs):
         """WebDriver-based executor for reftests"""
         RefTestExecutor.__init__(self,
                                  browser,
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/runner.js
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/runner.js
@@ -1,1 +1,59 @@
 document.title = '%(title)s';
+
+window.addEventListener(
+  "message",
+  function(event) {
+    window.message_queue.push(event);
+    window.process_next_event();
+  },
+  false
+);
+
+
+window.process_next_event = function() {
+  /* This function handles the next testdriver event. The presence of
+     window.testdriver_callback is used as a switch; when that function
+     is present we are able to handle the next event and when is is not
+     present we must wait. Therefore to drive the event processing, this
+     function must be called in two circumstances:
+       * Every time there is a new event that we may be able to handle
+       * Every time we set the callback function
+     This function unsets the callback, so no further testdriver actions
+     will be run until it is reset, which wptrunner does after it has
+     completed handling the current action.
+   */
+  if (!window.testdriver_callback) {
+    return;
+  }
+  var event = window.message_queue.shift();
+  if (!event) {
+    return;
+  }
+  var data = event.data;
+
+  var payload = undefined;
+
+  switch(data.type) {
+  case "complete":
+    var tests = event.data.tests;
+    var status = event.data.status;
+
+    var subtest_results = tests.map(function(x) {
+      return [x.name, x.status, x.message, x.stack];
+    });
+    payload = [status.status,
+               status.message,
+               status.stack,
+               subtest_results];
+    clearTimeout(window.timer);
+    break;
+  case "action":
+    payload = data;
+    break;
+  default:
+    return;
+  }
+  var callback = window.testdriver_callback;
+  window.testdriver_callback = null;
+  callback([window.url, data.type, payload]);
+};
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/testharness_webdriver.js
@@ -0,0 +1,19 @@
+var callback = arguments[arguments.length - 1];
+var loaded = false;
+
+window.timeout_multiplier = %(timeout_multiplier)d;
+window.url = "%(url)s";
+window.win = window.open("%(abs_url)s", "%(window_id)s");
+window.win.addEventListener('DOMContentLoaded', (e) => {
+  callback();
+});
+
+
+window.message_queue = [];
+window.testdriver_callback = null;
+
+if (%(timeout)s != null) {
+  window.timer = setTimeout(function() {
+    window.win.timeout();
+  }, %(timeout)s);
+}
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/testharness_webdriver_resume.js
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/executors/testharness_webdriver_resume.js
@@ -1,5 +1,21 @@
-// We have to set the url here to ensure we get the same escaping as in the harness
-// and also to handle the case where the test changes the fragment
-window.__wptrunner_url = "%(url)s";
-window.__wptrunner_testdriver_callback = arguments[arguments.length - 1];
-window.__wptrunner_process_next_event();
+var callback = arguments[arguments.length - 1];
+window.opener.testdriver_callback = function(results) {
+  /**
+   * The current window and its opener belong to the same domain, making it
+   * technically possible for data structures to be shared directly.
+   * Unfortunately, some browser/WebDriver implementations incorrectly
+   * serialize Arrays from foreign realms [1]. This issue does not extend to
+   * the behavior of `JSON.stringify` and `JSON.parse` in these
+   * implementations. Use that API to re-create the data structure in the local
+   * realm to avoid the problem in the non-conforming browsers.
+   *
+   * [1] This has been observed in Edge version 17 and/or the corresponding
+   *     release of Edgedriver
+   */
+  try {
+    results = JSON.parse(JSON.stringify(results));
+  } catch (error) {}
+
+  callback(results);
+};
+window.opener.process_next_event();
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/products.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/products.py
@@ -1,71 +1,58 @@
 import importlib
 import imp
 
 from .browsers import product_list
 
-
 def products_enabled(config):
     names = config.get("products", {}).keys()
     if not names:
         return product_list
     else:
         return names
 
-
 def product_module(config, product):
     if product not in products_enabled(config):
         raise ValueError("Unknown product %s" % product)
 
     path = config.get("products", {}).get(product, None)
     if path:
         module = imp.load_source('wptrunner.browsers.' + product, path)
     else:
         module = importlib.import_module("wptrunner.browsers." + product)
 
     if not hasattr(module, "__wptrunner__"):
         raise ValueError("Product module does not define __wptrunner__ variable")
 
     return module
 
 
-class Product(object):
-    def __init__(self, config, product):
-        module = product_module(config, product)
-        data = module.__wptrunner__
-        self.name = product
-        self.check_args = getattr(module, data["check_args"])
-        self.browser_cls = getattr(module, data["browser"])
-        self.get_browser_kwargs = getattr(module, data["browser_kwargs"])
-        self.get_executor_kwargs = getattr(module, data["executor_kwargs"])
-        self.env_options = getattr(module, data["env_options"])()
-        self.get_env_extras = getattr(module, data["env_extras"])
-        self.run_info_extras = (getattr(module, data["run_info_extras"])
-                           if "run_info_extras" in data else lambda **kwargs:{})
-        self.get_timeout_multiplier = getattr(module, data["timeout_multiplier"])
+def load_product(config, product):
+    module = product_module(config, product)
+    data = module.__wptrunner__
 
-        self.executor_classes = {}
-        for test_type, cls_name in data["executor"].iteritems():
-            cls = getattr(module, cls_name)
-            self.executor_classes[test_type] = cls
-
+    check_args = getattr(module, data["check_args"])
+    browser_cls = getattr(module, data["browser"])
+    browser_kwargs = getattr(module, data["browser_kwargs"])
+    executor_kwargs = getattr(module, data["executor_kwargs"])
+    env_options = getattr(module, data["env_options"])()
+    env_extras = getattr(module, data["env_extras"])
+    run_info_extras = (getattr(module, data["run_info_extras"])
+                       if "run_info_extras" in data else lambda **kwargs:{})
 
-def load_product(config, product, load_cls=False):
-    rv = Product(config, product)
-    if not load_cls:
-        return (rv.check_args,
-                rv.browser_cls,
-                rv.get_browser_kwargs,
-                rv.executor_classes,
-                rv.get_executor_kwargs,
-                rv.env_options,
-                rv.get_env_extras,
-                rv.run_info_extras)
-    return rv
+    executor_classes = {}
+    for test_type, cls_name in data["executor"].iteritems():
+        cls = getattr(module, cls_name)
+        executor_classes[test_type] = cls
+
+    return (check_args,
+            browser_cls, browser_kwargs,
+            executor_classes, executor_kwargs,
+            env_options, env_extras, run_info_extras)
 
 
 def load_product_update(config, product):
     """Return tuple of (property_order, boolean_properties) indicating the
     run_info properties to use when constructing the expectation data for
     this product. None for either key indicates that the default keys
     appropriate for distinguishing based on platform will be used."""
 
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/testdriver-extra.js
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/testdriver-extra.js
@@ -52,27 +52,27 @@
     };
 
     window.test_driver_internal.click = function(element) {
         const selector = get_selector(element);
         const pending_promise = new Promise(function(resolve, reject) {
             pending_resolve = resolve;
             pending_reject = reject;
         });
-        window.__wptrunner_message_queue.push({"type": "action", "action": "click", "selector": selector});
+        window.opener.postMessage({"type": "action", "action": "click", "selector": selector}, "*");
         return pending_promise;
     };
 
     window.test_driver_internal.send_keys = function(element, keys) {
         const selector = get_selector(element);
         const pending_promise = new Promise(function(resolve, reject) {
             pending_resolve = resolve;
             pending_reject = reject;
         });
-        window.__wptrunner_message_queue.push({"type": "action", "action": "send_keys", "selector": selector, "keys": keys});
+        window.opener.postMessage({"type": "action", "action": "send_keys", "selector": selector, "keys": keys}, "*");
         return pending_promise;
     };
 
     window.test_driver_internal.action_sequence = function(actions) {
         const pending_promise = new Promise(function(resolve, reject) {
             pending_resolve = resolve;
             pending_reject = reject;
         });
@@ -80,12 +80,12 @@
             if (actionSequence.type == "pointer") {
                 for (let action of actionSequence.actions) {
                     if (action.type == "pointerMove" && action.origin instanceof Element) {
                         action.origin = {selector: get_selector(action.origin)};
                     }
                 }
             }
         }
-        window.__wptrunner_message_queue.push({"type": "action", "action": "action_sequence", "actions": actions});
+        window.opener.postMessage({"type": "action", "action": "action_sequence", "actions": actions}, "*");
         return pending_promise;
     };
 })();
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/testharnessreport.js
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/testharnessreport.js
@@ -1,68 +1,13 @@
-window.__wptrunner_testdriver_callback = null;
-window.__wptrunner_message_queue = [];
-window.__wptrunner_url = null;
-
-window.__wptrunner_process_next_event = function() {
-  /* This function handles the next testdriver event. The presence of
-     window.testdriver_callback is used as a switch; when that function
-     is present we are able to handle the next event and when is is not
-     present we must wait. Therefore to drive the event processing, this
-     function must be called in two circumstances:
-       * Every time there is a new event that we may be able to handle
-       * Every time we set the callback function
-     This function unsets the callback, so no further testdriver actions
-     will be run until it is reset, which wptrunner does after it has
-     completed handling the current action.
-   */
-
-  if (!window.__wptrunner_testdriver_callback) {
-    return;
-  }
-  var data = window.__wptrunner_message_queue.shift();
-  if (!data) {
-    return;
-  }
-
-  var payload = undefined;
-
-  switch(data.type) {
-  case "complete":
-    var tests = data.tests;
-    var status = data.status;
+var props = {output:%(output)d,
+             explicit_timeout: true,
+             message_events: ["completion"]};
 
-    var subtest_results = tests.map(function(x) {
-      return [x.name, x.status, x.message, x.stack];
-    });
-    payload = [status.status,
-               status.message,
-               status.stack,
-               subtest_results];
-    clearTimeout(window.__wptrunner_timer);
-    break;
-  case "action":
-    payload = data;
-    break;
-  default:
-    return;
-  }
-  var callback = window.__wptrunner_testdriver_callback;
-  window.__wptrunner_testdriver_callback = null;
-  callback([__wptrunner_url, data.type, payload]);
-};
+if (window.opener && "timeout_multiplier" in window.opener) {
+    props["timeout_multiplier"] = window.opener.timeout_multiplier;
+}
 
-(function() {
-  var props = {output: %(output)d,
-               timeout_multiplier: %(timeout_multiplier)s,
-               explicit_timeout: %(explicit_timeout)s,
-               message_events: ["completion"]};
+if (window.opener && window.opener.explicit_timeout) {
+    props["explicit_timeout"] = window.opener.explicit_timeout;
+}
 
-  add_completion_callback(function(tests, harness_status) {
-    __wptrunner_message_queue.push({
-      "type": "complete",
-      "tests": tests,
-      "status": harness_status});
-    __wptrunner_process_next_event();
-  });
-  setup(props);
-})();
-
+setup(props);
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/tests/test_products.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/tests/test_products.py
@@ -41,17 +41,16 @@ def test_server_start_config(product):
      target_browser_cls, get_browser_kwargs,
      executor_classes, get_executor_kwargs,
      env_options, get_env_extras, run_info_extras) = products.load_product({}, product)
 
     env_extras = get_env_extras()
 
     with mock.patch.object(environment.serve, "start") as start:
         with environment.TestEnvironment(test_paths,
-                                         1,
                                          False,
                                          None,
                                          env_options,
                                          {"type": "none"},
                                          env_extras):
             start.assert_called_once()
             args = start.call_args
             config = args[0][0]
--- a/testing/web-platform/tests/tools/wptrunner/wptrunner/wptrunner.py
+++ b/testing/web-platform/tests/tools/wptrunner/wptrunner/wptrunner.py
@@ -133,31 +133,34 @@ def get_pause_after_test(test_loader, **
         return False
     return kwargs["pause_after_test"]
 
 
 def run_tests(config, test_paths, product, **kwargs):
     with wptlogging.CaptureIO(logger, not kwargs["no_capture_stdio"]):
         env.do_delayed_imports(logger, test_paths)
 
-        product = products.load_product(config, product, load_cls=True)
+        (check_args,
+         target_browser_cls, get_browser_kwargs,
+         executor_classes, get_executor_kwargs,
+         env_options, get_env_extras, run_info_extras) = products.load_product(config, product)
 
-        env_extras = product.get_env_extras(**kwargs)
+        env_extras = get_env_extras(**kwargs)
 
-        product.check_args(**kwargs)
+        check_args(**kwargs)
 
         if kwargs["install_fonts"]:
             env_extras.append(FontInstaller(
                 font_dir=kwargs["font_dir"],
                 ahem=os.path.join(test_paths["/"]["tests_path"], "fonts/Ahem.ttf")
             ))
 
         run_info, test_loader = get_loader(test_paths,
-                                           product.name,
-                                           run_info_extras=product.run_info_extras(**kwargs),
+                                           product,
+                                           run_info_extras=run_info_extras(**kwargs),
                                            **kwargs)
 
         test_source_kwargs = {"processes": kwargs["processes"]}
         if kwargs["run_by_dir"] is False:
             test_source_cls = testloader.SingleTestSource
         else:
             # A value of None indicates infinite depth
             test_source_cls = testloader.PathGroupedSource
@@ -178,23 +181,20 @@ def run_tests(config, test_paths, produc
         kwargs["pause_after_test"] = get_pause_after_test(test_loader, **kwargs)
 
         ssl_config = {"type": kwargs["ssl_type"],
                       "openssl": {"openssl_binary": kwargs["openssl_binary"]},
                       "pregenerated": {"host_key_path": kwargs["host_key_path"],
                                        "host_cert_path": kwargs["host_cert_path"],
                                        "ca_cert_path": kwargs["ca_cert_path"]}}
 
-        testharness_timeout_multipler = product.get_timeout_multiplier("testharness", run_info, **kwargs)
-
         with env.TestEnvironment(test_paths,
-                                 testharness_timeout_multipler,
                                  kwargs["pause_after_test"],
                                  kwargs["debug_info"],
-                                 product.env_options,
+                                 env_options,
                                  ssl_config,
                                  env_extras) as test_environment:
             try:
                 test_environment.ensure_started()
             except env.TestEnvironmentError as e:
                 logger.critical("Error starting test environment: %s" % e.message)
                 raise
 
@@ -206,60 +206,58 @@ def run_tests(config, test_paths, produc
                 repeat_count += 1
                 if repeat_until_unexpected:
                     logger.info("Repetition %i" % (repeat_count))
                 elif repeat > 1:
                     logger.info("Repetition %i / %i" % (repeat_count, repeat))
 
                 test_count = 0
                 unexpected_count = 0
-                logger.suite_start(test_loader.test_ids,
-                                   name='web-platform-test',
-                                   run_info=run_info,
+                logger.suite_start(test_loader.test_ids, name='web-platform-test', run_info=run_info,
                                    extra={"run_by_dir": kwargs["run_by_dir"]})
                 for test_type in kwargs["test_types"]:
                     logger.info("Running %s tests" % test_type)
 
                     # WebDriver tests may create and destroy multiple browser
                     # processes as part of their expected behavior. These
                     # processes are managed by a WebDriver server binary. This
                     # obviates the need for wptrunner to provide a browser, so
                     # the NullBrowser is used in place of the "target" browser
                     if test_type == "wdspec":
                         browser_cls = NullBrowser
                     else:
-                        browser_cls = product.browser_cls
+                        browser_cls = target_browser_cls
 
-                    browser_kwargs = product.get_browser_kwargs(test_type,
-                                                                run_info,
-                                                                config=test_environment.config,
-                                                                **kwargs)
+                    browser_kwargs = get_browser_kwargs(test_type,
+                                                        run_info,
+                                                        config=test_environment.config,
+                                                        **kwargs)
 
-                    executor_cls = product.executor_classes.get(test_type)
-                    executor_kwargs = product.get_executor_kwargs(test_type,
-                                                                  test_environment.config,
-                                                                  test_environment.cache_manager,
-                                                                  run_info,
-                                                                  **kwargs)
+                    executor_cls = executor_classes.get(test_type)
+                    executor_kwargs = get_executor_kwargs(test_type,
+                                                          test_environment.config,
+                                                          test_environment.cache_manager,
+                                                          run_info,
+                                                          **kwargs)
 
                     if executor_cls is None:
                         logger.error("Unsupported test type %s for product %s" %
-                                     (test_type, product.name))
+                                     (test_type, product))
                         continue
 
                     for test in test_loader.disabled_tests[test_type]:
                         logger.test_start(test.id)
                         logger.test_end(test.id, status="SKIP")
                         skipped_tests += 1
 
                     if test_type == "testharness":
                         run_tests = {"testharness": []}
                         for test in test_loader.tests["testharness"]:
-                            if ((test.testdriver and not executor_cls.supports_testdriver) or
-                                (test.jsshell and not executor_cls.supports_jsshell)):
+                            if (test.testdriver and not executor_cls.supports_testdriver) or (
+                                    test.jsshell and not executor_cls.supports_jsshell):
                                 logger.test_start(test.id)
                                 logger.test_end(test.id, status="SKIP")
                                 skipped_tests += 1
                             else:
                                 run_tests["testharness"].append(test)
                     else:
                         run_tests = test_loader.tests
 
--- a/testing/web-platform/tests/tools/wptserve/wptserve/stash.py
+++ b/testing/web-platform/tests/tools/wptserve/wptserve/stash.py
@@ -1,69 +1,60 @@
 import base64
 import json
 import os
 import uuid
 import threading
 from multiprocessing.managers import AcquirerProxy, BaseManager, DictProxy
 from six import text_type
 
-
 class ServerDictManager(BaseManager):
     shared_data = {}
 
-
 def _get_shared():
     return ServerDictManager.shared_data
 
-
 ServerDictManager.register("get_dict",
                            callable=_get_shared,
                            proxytype=DictProxy)
 ServerDictManager.register('Lock', threading.Lock, AcquirerProxy)
 
-
 class ClientDictManager(BaseManager):
     pass
 
-
 ClientDictManager.register("get_dict")
 ClientDictManager.register("Lock")
 
-
 class StashServer(object):
     def __init__(self, address=None, authkey=None):
         self.address = address
         self.authkey = authkey
         self.manager = None
 
     def __enter__(self):
         self.manager, self.address, self.authkey = start_server(self.address, self.authkey)
         store_env_config(self.address, self.authkey)
 
     def __exit__(self, *args, **kwargs):
         if self.manager is not None:
             self.manager.shutdown()
 
-
 def load_env_config():
     address, authkey = json.loads(os.environ["WPT_STASH_CONFIG"])
     if isinstance(address, list):
         address = tuple(address)
     else:
         address = str(address)
     authkey = base64.b64decode(authkey)
     return address, authkey
 
-
 def store_env_config(address, authkey):
     authkey = base64.b64encode(authkey)
     os.environ["WPT_STASH_CONFIG"] = json.dumps((address, authkey.decode("ascii")))
 
-
 def start_server(address=None, authkey=None):
     if isinstance(authkey, text_type):
         authkey = authkey.encode("ascii")
     manager = ServerDictManager(address, authkey)
     manager.start()
 
     return (manager, manager._address, manager._authkey)
 
@@ -79,17 +70,16 @@ class LockWrapper(object):
         self.lock.release()
 
     def __enter__(self):
         self.acquire()
 
     def __exit__(self, *args, **kwargs):
         self.release()
 
-
 #TODO: Consider expiring values after some fixed time for long-running
 #servers
 
 class Stash(object):
     """Key-value store for persisting data across HTTP/S and WS/S requests.
 
     This data store is specifically designed for persisting data across server
     requests. The synchronization is achieved by using the BaseManager from
@@ -178,11 +168,10 @@ class Stash(object):
             try:
                 self.data.pop(internal_key)
             except KeyError:
                 # Silently continue when pop error occurs.
                 pass
 
         return value
 
-
 class StashError(Exception):
     pass