Unreviewed. Update W3C WebDriver imported tests.

Tools:

* Scripts/webkitpy/webdriver_tests/webdriver_w3c_executor.py:
(WebDriverW3CExecutor.__init__):

WebDriverTests:

* imported/w3c/importer.json:
* imported/w3c/tools/webdriver/webdriver/client.py:
* imported/w3c/tools/webdriver/webdriver/transport.py:
* imported/w3c/tools/wptrunner/README.rst:
* imported/w3c/tools/wptrunner/requirements_chrome.txt:
* imported/w3c/tools/wptrunner/requirements_edge.txt:
* imported/w3c/tools/wptrunner/requirements_firefox.txt:
* imported/w3c/tools/wptrunner/requirements_ie.txt:
* imported/w3c/tools/wptrunner/requirements_opera.txt:
* imported/w3c/tools/wptrunner/requirements_safari.txt: Added.
* imported/w3c/tools/wptrunner/requirements_sauce.txt:
* imported/w3c/tools/wptrunner/requirements_webkit.txt: Added.
* imported/w3c/tools/wptrunner/tox.ini:
* imported/w3c/tools/wptrunner/wptrunner/browsers/__init__.py:
* imported/w3c/tools/wptrunner/wptrunner/browsers/chrome.py:
* imported/w3c/tools/wptrunner/wptrunner/browsers/chrome_android.py:
* imported/w3c/tools/wptrunner/wptrunner/browsers/edge.py:
* imported/w3c/tools/wptrunner/wptrunner/browsers/firefox.py:
* imported/w3c/tools/wptrunner/wptrunner/browsers/ie.py:
* imported/w3c/tools/wptrunner/wptrunner/browsers/opera.py:
* imported/w3c/tools/wptrunner/wptrunner/browsers/safari.py: Added.
* imported/w3c/tools/wptrunner/wptrunner/browsers/sauce.py:
* imported/w3c/tools/wptrunner/wptrunner/browsers/sauce_setup/edge-prerun.bat:
* imported/w3c/tools/wptrunner/wptrunner/browsers/sauce_setup/safari-prerun.sh:
* imported/w3c/tools/wptrunner/wptrunner/browsers/servo.py:
* imported/w3c/tools/wptrunner/wptrunner/browsers/servodriver.py:
* imported/w3c/tools/wptrunner/wptrunner/browsers/webkit.py: Added.
* imported/w3c/tools/wptrunner/wptrunner/environment.py:
* imported/w3c/tools/wptrunner/wptrunner/executors/base.py:
* imported/w3c/tools/wptrunner/wptrunner/executors/executormarionette.py:
* imported/w3c/tools/wptrunner/wptrunner/executors/executorselenium.py:
* imported/w3c/tools/wptrunner/wptrunner/executors/executorservo.py:
* imported/w3c/tools/wptrunner/wptrunner/executors/executorservodriver.py:
* imported/w3c/tools/wptrunner/wptrunner/executors/executorwebkit.py: Added.
* imported/w3c/tools/wptrunner/wptrunner/executors/protocol.py: Added.
* imported/w3c/tools/wptrunner/wptrunner/executors/pytestrunner/runner.py:
* imported/w3c/tools/wptrunner/wptrunner/executors/testharness_marionette.js:
* imported/w3c/tools/wptrunner/wptrunner/executors/testharness_marionette_resume.js: Added.
* imported/w3c/tools/wptrunner/wptrunner/executors/testharness_webdriver_resume.js:
* imported/w3c/tools/wptrunner/wptrunner/testdriver-extra.js:
* imported/w3c/tools/wptrunner/wptrunner/testloader.py:
* imported/w3c/tools/wptrunner/wptrunner/testrunner.py:
* imported/w3c/tools/wptrunner/wptrunner/tests/base.py: Added.
* imported/w3c/tools/wptrunner/wptrunner/tests/test_products.py: Added.
* imported/w3c/tools/wptrunner/wptrunner/update/metadata.py:
* imported/w3c/tools/wptrunner/wptrunner/update/update.py:
* imported/w3c/tools/wptrunner/wptrunner/webdriver_server.py:
* imported/w3c/tools/wptrunner/wptrunner/wptcommandline.py:
* imported/w3c/tools/wptrunner/wptrunner/wptrunner.py:
* imported/w3c/tools/wptrunner/wptrunner/wpttest.py:
* imported/w3c/webdriver/tests/actions/modifier_click.py:
* imported/w3c/webdriver/tests/actions/mouse_dblclick.py:
* imported/w3c/webdriver/tests/actions/mouse_pause_dblclick.py: Added.
* imported/w3c/webdriver/tests/actions/special_keys.py:
* imported/w3c/webdriver/tests/contexts/maximize_window.py:
* imported/w3c/webdriver/tests/cookies/add_cookie.py:
* imported/w3c/webdriver/tests/element_retrieval/find_element.py:
* imported/w3c/webdriver/tests/element_retrieval/find_element_from_element.py:
* imported/w3c/webdriver/tests/element_retrieval/find_elements.py:
* imported/w3c/webdriver/tests/element_retrieval/find_elements_from_element.py:
* imported/w3c/webdriver/tests/element_send_keys/form_controls.py:
* imported/w3c/webdriver/tests/execute_async_script/collections.py: Added.
* imported/w3c/webdriver/tests/execute_async_script/user_prompts.py:
* imported/w3c/webdriver/tests/execute_script/collections.py: Added.
* imported/w3c/webdriver/tests/execute_script/user_prompts.py:
* imported/w3c/webdriver/tests/fullscreen_window.py:
* imported/w3c/webdriver/tests/interaction/element_clear.py:
* imported/w3c/webdriver/tests/minimize_window.py:
* imported/w3c/webdriver/tests/set_window_rect.py:
* imported/w3c/webdriver/tests/support/fixtures.py:

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@230953 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/Tools/ChangeLog b/Tools/ChangeLog
index 369a13c..f9a74df 100644
--- a/Tools/ChangeLog
+++ b/Tools/ChangeLog
@@ -1,3 +1,10 @@
+2018-04-24  Carlos Garcia Campos  <cgarcia@igalia.com>
+
+        Unreviewed. Update W3C WebDriver imported tests.
+
+        * Scripts/webkitpy/webdriver_tests/webdriver_w3c_executor.py:
+        (WebDriverW3CExecutor.__init__):
+
 2018-04-24  Zan Dobersek  <zdobersek@igalia.com>
 
         [WPE] Remove libgbm dependency
diff --git a/Tools/Scripts/webkitpy/webdriver_tests/webdriver_w3c_executor.py b/Tools/Scripts/webkitpy/webdriver_tests/webdriver_w3c_executor.py
index 6461704..c54e062 100644
--- a/Tools/Scripts/webkitpy/webdriver_tests/webdriver_w3c_executor.py
+++ b/Tools/Scripts/webkitpy/webdriver_tests/webdriver_w3c_executor.py
@@ -133,7 +133,7 @@
     def __init__(self, driver, server, env, timeout, expectations):
         WebKitDriverServer.test_env = env
         WebKitDriverServer.test_env.update(driver.browser_env())
-        server_config = {'host': server.host(), 'domains': {'': server.host()}, 'ports': {'http': [str(server.port())]}}
+        server_config = {'browser_host': server.host(), 'domains': {'': server.host()}, 'ports': {'http': [str(server.port())]}}
         WdspecExecutor.__init__(self, driver.browser_name(), server_config, driver.binary_path(), None, capabilities=driver.capabilities())
 
         self._timeout = timeout
diff --git a/WebDriverTests/ChangeLog b/WebDriverTests/ChangeLog
index fc30140..4838a27 100644
--- a/WebDriverTests/ChangeLog
+++ b/WebDriverTests/ChangeLog
@@ -1,3 +1,78 @@
+2018-04-24  Carlos Garcia Campos  <cgarcia@igalia.com>
+
+        Unreviewed. Update W3C WebDriver imported tests.
+
+        * imported/w3c/importer.json:
+        * imported/w3c/tools/webdriver/webdriver/client.py:
+        * imported/w3c/tools/webdriver/webdriver/transport.py:
+        * imported/w3c/tools/wptrunner/README.rst:
+        * imported/w3c/tools/wptrunner/requirements_chrome.txt:
+        * imported/w3c/tools/wptrunner/requirements_edge.txt:
+        * imported/w3c/tools/wptrunner/requirements_firefox.txt:
+        * imported/w3c/tools/wptrunner/requirements_ie.txt:
+        * imported/w3c/tools/wptrunner/requirements_opera.txt:
+        * imported/w3c/tools/wptrunner/requirements_safari.txt: Added.
+        * imported/w3c/tools/wptrunner/requirements_sauce.txt:
+        * imported/w3c/tools/wptrunner/requirements_webkit.txt: Added.
+        * imported/w3c/tools/wptrunner/tox.ini:
+        * imported/w3c/tools/wptrunner/wptrunner/browsers/__init__.py:
+        * imported/w3c/tools/wptrunner/wptrunner/browsers/chrome.py:
+        * imported/w3c/tools/wptrunner/wptrunner/browsers/chrome_android.py:
+        * imported/w3c/tools/wptrunner/wptrunner/browsers/edge.py:
+        * imported/w3c/tools/wptrunner/wptrunner/browsers/firefox.py:
+        * imported/w3c/tools/wptrunner/wptrunner/browsers/ie.py:
+        * imported/w3c/tools/wptrunner/wptrunner/browsers/opera.py:
+        * imported/w3c/tools/wptrunner/wptrunner/browsers/safari.py: Added.
+        * imported/w3c/tools/wptrunner/wptrunner/browsers/sauce.py:
+        * imported/w3c/tools/wptrunner/wptrunner/browsers/sauce_setup/edge-prerun.bat:
+        * imported/w3c/tools/wptrunner/wptrunner/browsers/sauce_setup/safari-prerun.sh:
+        * imported/w3c/tools/wptrunner/wptrunner/browsers/servo.py:
+        * imported/w3c/tools/wptrunner/wptrunner/browsers/servodriver.py:
+        * imported/w3c/tools/wptrunner/wptrunner/browsers/webkit.py: Added.
+        * imported/w3c/tools/wptrunner/wptrunner/environment.py:
+        * imported/w3c/tools/wptrunner/wptrunner/executors/base.py:
+        * imported/w3c/tools/wptrunner/wptrunner/executors/executormarionette.py:
+        * imported/w3c/tools/wptrunner/wptrunner/executors/executorselenium.py:
+        * imported/w3c/tools/wptrunner/wptrunner/executors/executorservo.py:
+        * imported/w3c/tools/wptrunner/wptrunner/executors/executorservodriver.py:
+        * imported/w3c/tools/wptrunner/wptrunner/executors/executorwebkit.py: Added.
+        * imported/w3c/tools/wptrunner/wptrunner/executors/protocol.py: Added.
+        * imported/w3c/tools/wptrunner/wptrunner/executors/pytestrunner/runner.py:
+        * imported/w3c/tools/wptrunner/wptrunner/executors/testharness_marionette.js:
+        * imported/w3c/tools/wptrunner/wptrunner/executors/testharness_marionette_resume.js: Added.
+        * imported/w3c/tools/wptrunner/wptrunner/executors/testharness_webdriver_resume.js:
+        * imported/w3c/tools/wptrunner/wptrunner/testdriver-extra.js:
+        * imported/w3c/tools/wptrunner/wptrunner/testloader.py:
+        * imported/w3c/tools/wptrunner/wptrunner/testrunner.py:
+        * imported/w3c/tools/wptrunner/wptrunner/tests/base.py: Added.
+        * imported/w3c/tools/wptrunner/wptrunner/tests/test_products.py: Added.
+        * imported/w3c/tools/wptrunner/wptrunner/update/metadata.py:
+        * imported/w3c/tools/wptrunner/wptrunner/update/update.py:
+        * imported/w3c/tools/wptrunner/wptrunner/webdriver_server.py:
+        * imported/w3c/tools/wptrunner/wptrunner/wptcommandline.py:
+        * imported/w3c/tools/wptrunner/wptrunner/wptrunner.py:
+        * imported/w3c/tools/wptrunner/wptrunner/wpttest.py:
+        * imported/w3c/webdriver/tests/actions/modifier_click.py:
+        * imported/w3c/webdriver/tests/actions/mouse_dblclick.py:
+        * imported/w3c/webdriver/tests/actions/mouse_pause_dblclick.py: Added.
+        * imported/w3c/webdriver/tests/actions/special_keys.py:
+        * imported/w3c/webdriver/tests/contexts/maximize_window.py:
+        * imported/w3c/webdriver/tests/cookies/add_cookie.py:
+        * imported/w3c/webdriver/tests/element_retrieval/find_element.py:
+        * imported/w3c/webdriver/tests/element_retrieval/find_element_from_element.py:
+        * imported/w3c/webdriver/tests/element_retrieval/find_elements.py:
+        * imported/w3c/webdriver/tests/element_retrieval/find_elements_from_element.py:
+        * imported/w3c/webdriver/tests/element_send_keys/form_controls.py:
+        * imported/w3c/webdriver/tests/execute_async_script/collections.py: Added.
+        * imported/w3c/webdriver/tests/execute_async_script/user_prompts.py:
+        * imported/w3c/webdriver/tests/execute_script/collections.py: Added.
+        * imported/w3c/webdriver/tests/execute_script/user_prompts.py:
+        * imported/w3c/webdriver/tests/fullscreen_window.py:
+        * imported/w3c/webdriver/tests/interaction/element_clear.py:
+        * imported/w3c/webdriver/tests/minimize_window.py:
+        * imported/w3c/webdriver/tests/set_window_rect.py:
+        * imported/w3c/webdriver/tests/support/fixtures.py:
+
 2018-03-27  Brian Burg  <bburg@apple.com>
 
         REGRESSION(r229937): WebDriver tests no longer run, test runner hangs when launching wpt web server
diff --git a/WebDriverTests/imported/w3c/importer.json b/WebDriverTests/imported/w3c/importer.json
index fb47860..572547c 100644
--- a/WebDriverTests/imported/w3c/importer.json
+++ b/WebDriverTests/imported/w3c/importer.json
@@ -1,6 +1,6 @@
 {
     "repository": "https://github.com/w3c/web-platform-tests.git",
-    "revision": "549b9cc8d48cfb683eecb3fd3909f93001d2e8f6",
+    "revision": "564cfb3a6bd13e949d5c49ad6e5f2cb3752173f3",
     "paths_to_import": [
         "tools/webdriver",
         "tools/wptrunner",
diff --git a/WebDriverTests/imported/w3c/tools/webdriver/webdriver/client.py b/WebDriverTests/imported/w3c/tools/webdriver/webdriver/client.py
index 622c5fc..8da14a0 100644
--- a/WebDriverTests/imported/w3c/tools/webdriver/webdriver/client.py
+++ b/WebDriverTests/imported/w3c/tools/webdriver/webdriver/client.py
@@ -6,10 +6,6 @@
 
 from six import string_types
 
-from mozlog import get_default_logger
-
-logger = get_default_logger()
-
 
 def command(func):
     def inner(self, *args, **kwargs):
@@ -371,6 +367,9 @@
         self.alert = UserPrompt(self)
         self.actions = Actions(self)
 
+    def __repr__(self):
+        return "<%s %s>" % (self.__class__.__name__, self.session_id or "(disconnected)")
+
     def __eq__(self, other):
         return (self.session_id is not None and isinstance(other, Session) and
                 self.session_id == other.session_id)
@@ -632,6 +631,9 @@
         assert id not in self.session._element_cache
         self.session._element_cache[self.id] = self
 
+    def __repr__(self):
+        return "<%s %s>" % (self.__class__.__name__, self.id)
+
     def __eq__(self, other):
         return (isinstance(other, Element) and self.id == other.id and
                 self.session == other.session)
diff --git a/WebDriverTests/imported/w3c/tools/webdriver/webdriver/transport.py b/WebDriverTests/imported/w3c/tools/webdriver/webdriver/transport.py
index 9a1670a..960cb37 100644
--- a/WebDriverTests/imported/w3c/tools/webdriver/webdriver/transport.py
+++ b/WebDriverTests/imported/w3c/tools/webdriver/webdriver/transport.py
@@ -131,13 +131,16 @@
         if body is None and method == "POST":
             body = {}
 
-        try:
-            payload = json.dumps(body, cls=encoder, **codec_kwargs)
-        except ValueError:
-            raise ValueError("Failed to encode request body as JSON:\n"
-                "%s" % json.dumps(body, indent=2))
-        if isinstance(payload, text_type):
-            payload = body.encode("utf-8")
+        payload = None
+        if body is not None:
+            try:
+                payload = json.dumps(body, cls=encoder, **codec_kwargs)
+            except ValueError:
+                raise ValueError("Failed to encode request body as JSON:\n"
+                    "%s" % json.dumps(body, indent=2))
+
+            if isinstance(payload, text_type):
+                payload = body.encode("utf-8")
 
         if headers is None:
             headers = {}
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/README.rst b/WebDriverTests/imported/w3c/tools/wptrunner/README.rst
index 4890e3a..8917922 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/README.rst
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/README.rst
@@ -179,7 +179,7 @@
 
   key: value
 
-Note that unlike ini files, only `:` is a valid seperator; `=` will
+Note that unlike ini files, only `:` is a valid separator; `=` will
 not work as expected. Key-value pairs may also have conditional
 values of the form::
 
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/requirements_chrome.txt b/WebDriverTests/imported/w3c/tools/wptrunner/requirements_chrome.txt
index 25cc584..b5e4806 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/requirements_chrome.txt
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/requirements_chrome.txt
@@ -1,2 +1,2 @@
 mozprocess == 0.26
-selenium == 3.9.0
+selenium==3.11.0
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/requirements_edge.txt b/WebDriverTests/imported/w3c/tools/wptrunner/requirements_edge.txt
index 25cc584..b5e4806 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/requirements_edge.txt
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/requirements_edge.txt
@@ -1,2 +1,2 @@
 mozprocess == 0.26
-selenium == 3.9.0
+selenium==3.11.0
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/requirements_firefox.txt b/WebDriverTests/imported/w3c/tools/wptrunner/requirements_firefox.txt
index 43413cf..79fde78 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/requirements_firefox.txt
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/requirements_firefox.txt
@@ -1,6 +1,8 @@
-marionette_driver == 2.5.0
-mozprofile == 0.29
+marionette_driver==2.6.0
+mozprofile==1.0.0
 mozprocess == 0.26
 mozcrash == 1.0
-mozrunner == 6.14
+mozrunner==6.15
 mozleak == 0.1
+mozinstall == 1.15
+mozdownload == 1.23
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/requirements_ie.txt b/WebDriverTests/imported/w3c/tools/wptrunner/requirements_ie.txt
index 25cc584..b5e4806 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/requirements_ie.txt
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/requirements_ie.txt
@@ -1,2 +1,2 @@
 mozprocess == 0.26
-selenium == 3.9.0
+selenium==3.11.0
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/requirements_opera.txt b/WebDriverTests/imported/w3c/tools/wptrunner/requirements_opera.txt
index 25cc584..b5e4806 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/requirements_opera.txt
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/requirements_opera.txt
@@ -1,2 +1,2 @@
 mozprocess == 0.26
-selenium == 3.9.0
+selenium==3.11.0
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/requirements_safari.txt b/WebDriverTests/imported/w3c/tools/wptrunner/requirements_safari.txt
new file mode 100644
index 0000000..b5e4806
--- /dev/null
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/requirements_safari.txt
@@ -0,0 +1,2 @@
+mozprocess == 0.26
+selenium==3.11.0
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/requirements_sauce.txt b/WebDriverTests/imported/w3c/tools/wptrunner/requirements_sauce.txt
index 51bce11..e0ae7e2 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/requirements_sauce.txt
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/requirements_sauce.txt
@@ -1,3 +1,3 @@
 mozprocess == 0.26
-selenium == 3.9.0
-requests
+selenium==3.11.0
+requests==2.18.4
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/requirements_webkit.txt b/WebDriverTests/imported/w3c/tools/wptrunner/requirements_webkit.txt
new file mode 100644
index 0000000..25cc584
--- /dev/null
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/requirements_webkit.txt
@@ -0,0 +1,2 @@
+mozprocess == 0.26
+selenium == 3.9.0
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/tox.ini b/WebDriverTests/imported/w3c/tools/wptrunner/tox.ini
index 9be8c0e..f63e2c0 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/tox.ini
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/tox.ini
@@ -2,7 +2,7 @@
 xfail_strict=true
 
 [tox]
-envlist = {py27,pypy}-{base,chrome,firefox,sauce,servo},py27-flake8
+envlist = {py27}-{base,chrome,edge,firefox,ie,opera,safari,sauce,servo},py27-flake8
 
 [testenv]
 deps =
@@ -12,12 +12,18 @@
      mock
      -r{toxinidir}/requirements.txt
      chrome: -r{toxinidir}/requirements_chrome.txt
+     edge: -r{toxinidir}/requirements_edge.txt
      firefox: -r{toxinidir}/requirements_firefox.txt
+     ie: -r{toxinidir}/requirements_ie.txt
+     opera: -r{toxinidir}/requirements_opera.txt
+     safari: -r{toxinidir}/requirements_safari.txt
      sauce: -r{toxinidir}/requirements_sauce.txt
      servo: -r{toxinidir}/requirements_servo.txt
 
 commands = pytest {posargs:--cov}
 
+setenv = CURRENT_TOX_ENV = {envname}
+
 [testenv:py27-flake8]
 # flake8 versions should be kept in sync across tools/tox.ini, tools/wpt/tox.ini, and tools/wptrunner/tox.ini
 deps =
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/__init__.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/__init__.py
index 1e74863..6f0c49e 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/__init__.py
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/__init__.py
@@ -27,7 +27,9 @@
                 "edge",
                 "firefox",
                 "ie",
+                "safari",
                 "sauce",
                 "servo",
                 "servodriver",
-                "opera"]
+                "opera",
+                "webkit"]
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/chrome.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/chrome.py
index 7f9a21c..e50c592 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/chrome.py
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/chrome.py
@@ -60,7 +60,7 @@
 
 
 def env_options():
-    return {"bind_hostname": "true"}
+    return {}
 
 
 class ChromeBrowser(Browser):
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/chrome_android.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/chrome_android.py
index 3e832cb..985b1fe 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/chrome_android.py
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/chrome_android.py
@@ -68,7 +68,7 @@
 
 
 def env_options():
-    return {"bind_hostname": "true"}
+    return {}
 
 
 class ChromeAndroidBrowser(Browser):
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/edge.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/edge.py
index 094405c..184ae32 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/edge.py
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/edge.py
@@ -38,8 +38,7 @@
     return []
 
 def env_options():
-    return {"bind_hostname": "true",
-            "supports_debugger": False}
+    return {"supports_debugger": False}
 
 class EdgeBrowser(Browser):
     used_ports = set()
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/firefox.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/firefox.py
index acec0ef..0a0ca7e 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/firefox.py
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/firefox.py
@@ -102,7 +102,7 @@
         if kwargs["binary_args"]:
             options["args"] = kwargs["binary_args"]
         options["prefs"] = {
-            "network.dns.localDomains": ",".join(server_config['domains'].values())
+            "network.dns.localDomains": ",".join(server_config.domains.itervalues())
         }
         capabilities["moz:firefoxOptions"] = options
     if kwargs["certutil_binary"] is None:
@@ -122,10 +122,8 @@
     # domains to localhost without relying on the network stack.
     #
     # https://github.com/w3c/web-platform-tests/pull/9480
-    return {"host": "127.0.0.1",
-            "external_host": "web-platform.test",
-            "bind_hostname": "false",
-            "certificate_domain": "web-platform.test",
+    return {"server_host": "127.0.0.1",
+            "bind_address": False,
             "supports_debugger": True}
 
 
@@ -135,8 +133,8 @@
 
 
 def update_properties():
-    return (["debug", "stylo", "e10s", "os", "version", "processor", "bits"],
-            {"debug", "e10s", "stylo"})
+    return (["debug", "webrender", "e10s", "os", "version", "processor", "bits"],
+            {"debug", "e10s", "webrender"})
 
 
 class FirefoxBrowser(Browser):
@@ -200,7 +198,7 @@
         self.profile = FirefoxProfile(preferences=preferences)
         self.profile.set_preferences({"marionette.port": self.marionette_port,
                                       "dom.disable_open_during_load": False,
-                                      "network.dns.localDomains": ",".join(self.config['domains'].values()),
+                                      "network.dns.localDomains": ",".join(self.config.domains.itervalues()),
                                       "network.proxy.type": 0,
                                       "places.history.enabled": False,
                                       "dom.send_after_paint_to_content": True,
@@ -304,12 +302,15 @@
 
     def on_output(self, line):
         """Write a line of output from the firefox process to the log"""
-        data = line.decode("utf8", "replace")
-        if self.stack_fixer:
-            data = self.stack_fixer(data)
-        self.logger.process_output(self.pid(),
-                                   data,
-                                   command=" ".join(self.runner.command))
+        if "GLib-GObject-CRITICAL" in line:
+            return
+        if line:
+            data = line.decode("utf8", "replace")
+            if self.stack_fixer:
+                data = self.stack_fixer(data)
+            self.logger.process_output(self.pid(),
+                                      data,
+                                      command=" ".join(self.runner.command))
 
     def is_alive(self):
         if self.runner:
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/ie.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/ie.py
index 9c0007e..97d96ec 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/ie.py
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/ie.py
@@ -42,8 +42,7 @@
     return []
 
 def env_options():
-    return {"bind_hostname": "true",
-            "supports_debugger": False}
+    return {"supports_debugger": False}
 
 class InternetExplorerBrowser(Browser):
     used_ports = set()
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/opera.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/opera.py
index 693a19a..bfacfa5 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/opera.py
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/opera.py
@@ -60,7 +60,7 @@
 
 
 def env_options():
-    return {"bind_hostname": "true"}
+    return {}
 
 
 class OperaBrowser(Browser):
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/safari.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/safari.py
new file mode 100644
index 0000000..3b99d22
--- /dev/null
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/safari.py
@@ -0,0 +1,84 @@
+from .base import Browser, ExecutorBrowser, require_arg
+from ..webdriver_server import SafariDriverServer
+from ..executors import executor_kwargs as base_executor_kwargs
+from ..executors.executorselenium import (SeleniumTestharnessExecutor,
+                                          SeleniumRefTestExecutor)
+
+
+__wptrunner__ = {"product": "safari",
+                 "check_args": "check_args",
+                 "browser": "SafariBrowser",
+                 "executor": {"testharness": "SeleniumTestharnessExecutor",
+                              "reftest": "SeleniumRefTestExecutor"},
+                 "browser_kwargs": "browser_kwargs",
+                 "executor_kwargs": "executor_kwargs",
+                 "env_extras": "env_extras",
+                 "env_options": "env_options"}
+
+
+def check_args(**kwargs):
+    require_arg(kwargs, "webdriver_binary")
+
+
+def browser_kwargs(test_type, run_info_data, **kwargs):
+    return {"webdriver_binary": kwargs["webdriver_binary"],
+            "webdriver_args": kwargs.get("webdriver_args")}
+
+
+def executor_kwargs(test_type, server_config, cache_manager, run_info_data,
+                    **kwargs):
+    from selenium.webdriver import DesiredCapabilities
+
+    executor_kwargs = base_executor_kwargs(test_type, server_config,
+                                           cache_manager, **kwargs)
+    executor_kwargs["close_after_done"] = True
+    executor_kwargs["capabilities"] = dict(DesiredCapabilities.SAFARI.items())
+    if kwargs["binary"] is not None:
+        raise ValueError("Safari doesn't support setting executable location")
+
+    return executor_kwargs
+
+
+def env_extras(**kwargs):
+    return []
+
+
+def env_options():
+    return {}
+
+
+class SafariBrowser(Browser):
+    """Safari is backed by safaridriver, which is supplied through
+    ``wptrunner.webdriver.SafariDriverServer``.
+    """
+
+    def __init__(self, logger, webdriver_binary, webdriver_args=None):
+        """Creates a new representation of Safari.  The `webdriver_binary`
+        argument gives the WebDriver binary to use for testing. (The browser
+        binary location cannot be specified, as Safari and SafariDriver are
+        coupled.)"""
+        Browser.__init__(self, logger)
+        self.server = SafariDriverServer(self.logger,
+                                         binary=webdriver_binary,
+                                         args=webdriver_args)
+
+    def start(self, **kwargs):
+        self.server.start(block=False)
+
+    def stop(self, force=False):
+        self.server.stop(force=force)
+
+    def pid(self):
+        return self.server.pid
+
+    def is_alive(self):
+        # TODO(ato): This only indicates the driver is alive,
+        # and doesn't say anything about whether a browser session
+        # is active.
+        return self.server.is_alive()
+
+    def cleanup(self):
+        self.stop()
+
+    def executor_browser(self):
+        return ExecutorBrowser, {"webdriver_url": self.server.url}
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/sauce.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/sauce.py
index 69ebc07..9ae2f7e 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/sauce.py
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/sauce.py
@@ -113,8 +113,7 @@
 
 
 def env_options():
-    return {"bind_hostname": "true",
-            "supports_debugger": False}
+    return {"supports_debugger": False}
 
 
 def get_tar(url, dest):
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/sauce_setup/edge-prerun.bat b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/sauce_setup/edge-prerun.bat
index 4554894..9d0878e 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/sauce_setup/edge-prerun.bat
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/sauce_setup/edge-prerun.bat
@@ -1,2 +1,9 @@
 @echo off
 reg add "HKCU\Software\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppContainer\Storage\microsoft.microsoftedge_8wekyb3d8bbwe\MicrosoftEdge\New Windows" /v "PopupMgr" /t REG_SZ /d no
+
+
+REM Download and install the Ahem font
+REM - https://wiki.saucelabs.com/display/DOCS/Downloading+Files+to+a+Sauce+Labs+Virtual+Machine+Prior+to+Testing
+REM - https://superuser.com/questions/201896/how-do-i-install-a-font-from-the-windows-command-prompt
+bitsadmin.exe /transfer "JobName" https://github.com/w3c/web-platform-tests/raw/master/fonts/Ahem.ttf "%WINDIR%\Fonts\Ahem.ttf"
+reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" /v "Ahem (TrueType)" /t REG_SZ /d Ahem.ttf /f
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/sauce_setup/safari-prerun.sh b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/sauce_setup/safari-prerun.sh
index 85c72e6..06c48bd 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/sauce_setup/safari-prerun.sh
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/sauce_setup/safari-prerun.sh
@@ -1,2 +1,3 @@
 #!/bin/bash
+curl https://raw.githubusercontent.com/w3c/web-platform-tests/master/fonts/Ahem.ttf > ~/Library/Fonts/Ahem.ttf
 defaults write com.apple.Safari com.apple.Safari.ContentPageGroupIdentifier.WebKit2JavaScriptCanOpenWindowsAutomatically -bool true
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/servo.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/servo.py
index f163631..dd54fb0 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/servo.py
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/servo.py
@@ -52,9 +52,8 @@
 
 
 def env_options():
-    return {"host": "127.0.0.1",
-            "external_host": "web-platform.test",
-            "bind_hostname": False,
+    return {"server_host": "127.0.0.1",
+            "bind_address": False,
             "testharnessreport": "testharnessreport-servo.js",
             "supports_debugger": True}
 
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/servodriver.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/servodriver.py
index 89504b2..f2ee00a 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/servodriver.py
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/servodriver.py
@@ -5,7 +5,7 @@
 
 from mozprocess import ProcessHandler
 
-from tools.serve.serve import make_hosts_file
+from serve.serve import make_hosts_file
 
 from .base import Browser, require_arg, get_free_port, browser_command, ExecutorBrowser
 from ..executors import executor_kwargs as base_executor_kwargs
@@ -53,9 +53,7 @@
 
 
 def env_options():
-    return {"host": "127.0.0.1",
-            "external_host": "web-platform.test",
-            "bind_hostname": "true",
+    return {"server_host": "127.0.0.1",
             "testharnessreport": "testharnessreport-servodriver.js",
             "supports_debugger": True}
 
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/webkit.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/webkit.py
new file mode 100644
index 0000000..7d95d43
--- /dev/null
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/browsers/webkit.py
@@ -0,0 +1,98 @@
+from .base import Browser, ExecutorBrowser, require_arg
+from ..executors import executor_kwargs as base_executor_kwargs
+from ..executors.executorselenium import (SeleniumTestharnessExecutor,
+                                          SeleniumRefTestExecutor)
+from ..executors.executorwebkit import WebKitDriverWdspecExecutor
+from ..webdriver_server import WebKitDriverServer
+
+
+__wptrunner__ = {"product": "webkit",
+                 "check_args": "check_args",
+                 "browser": "WebKitBrowser",
+                 "browser_kwargs": "browser_kwargs",
+                 "executor": {"testharness": "SeleniumTestharnessExecutor",
+                              "reftest": "SeleniumRefTestExecutor",
+                              "wdspec": "WebKitDriverWdspecExecutor"},
+                 "executor_kwargs": "executor_kwargs",
+                 "env_extras": "env_extras",
+                 "env_options": "env_options"}
+
+
+def check_args(**kwargs):
+    require_arg(kwargs, "binary")
+    require_arg(kwargs, "webdriver_binary")
+    require_arg(kwargs, "webkit_port")
+
+
+def browser_kwargs(test_type, run_info_data, **kwargs):
+    return {"binary": kwargs["binary"],
+            "webdriver_binary": kwargs["webdriver_binary"],
+            "webdriver_args": kwargs.get("webdriver_args")}
+
+
+def capabilities_for_port(webkit_port, binary, binary_args):
+    from selenium.webdriver import DesiredCapabilities
+
+    if webkit_port == "gtk":
+        capabilities = dict(DesiredCapabilities.WEBKITGTK.copy())
+        capabilities["webkitgtk:browserOptions"] = {
+            "binary": binary,
+            "args": binary_args
+        }
+        return capabilities
+
+    return {}
+
+
+def executor_kwargs(test_type, server_config, cache_manager, run_info_data,
+                    **kwargs):
+    executor_kwargs = base_executor_kwargs(test_type, server_config,
+                                           cache_manager, **kwargs)
+    executor_kwargs["close_after_done"] = True
+    capabilities = capabilities_for_port(kwargs["webkit_port"],
+                                         kwargs["binary"],
+                                         kwargs.get("binary_args", []))
+    executor_kwargs["capabilities"] = capabilities
+    return executor_kwargs
+
+
+def env_extras(**kwargs):
+    return []
+
+
+def env_options():
+    return {}
+
+
+class WebKitBrowser(Browser):
+    """Generic WebKit browser is backed by WebKit's WebDriver implementation,
+    which is supplied through ``wptrunner.webdriver.WebKitDriverServer``.
+    """
+
+    def __init__(self, logger, binary, webdriver_binary=None,
+                 webdriver_args=None):
+        Browser.__init__(self, logger)
+        self.binary = binary
+        self.server = WebKitDriverServer(self.logger, binary=webdriver_binary,
+                                         args=webdriver_args)
+
+    def start(self, **kwargs):
+        self.server.start(block=False)
+
+    def stop(self, force=False):
+        self.server.stop(force=force)
+
+    def pid(self):
+        return self.server.pid
+
+    def is_alive(self):
+        # TODO(ato): This only indicates the driver is alive,
+        # and doesn't say anything about whether a browser session
+        # is active.
+        return self.server.is_alive()
+
+    def cleanup(self):
+        self.stop()
+
+    def executor_browser(self):
+        return ExecutorBrowser, {"webdriver_url": self.server.url}
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/environment.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/environment.py
index 8ecbb4b..c5e31b7 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/environment.py
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/environment.py
@@ -94,8 +94,6 @@
 
         self.config = self.load_config()
         self.setup_server_logging()
-        ports = serve.get_ports(self.config, self.ssl_env)
-        self.config = serve.normalise_config(self.config, ports)
 
         assert self.env_extras_cms is None, (
             "A TestEnvironment object cannot be nested")
@@ -107,7 +105,8 @@
             cm.__enter__()
             self.env_extras_cms.append(cm)
 
-        self.servers = serve.start(self.config, self.ssl_env,
+        self.servers = serve.start(self.config,
+                                   self.ssl_env,
                                    self.get_routes())
         if self.options.get("supports_debugger") and self.debug_info and self.debug_info.interactive:
             self.ignore_interrupts()
@@ -136,43 +135,36 @@
 
     def load_config(self):
         default_config_path = os.path.join(serve_path(self.test_paths), "config.default.json")
-        local_config = {
-            "ports": {
-                "http": [8000, 8001],
-                "https": [8443],
-                "ws": [8888]
-            },
-            "check_subdomains": False,
-            "bind_hostname": self.options["bind_hostname"],
-            "ssl": {}
-        }
-
-        if "host" in self.options:
-            local_config["host"] = self.options["host"]
+        override_path = os.path.join(serve_path(self.test_paths), "config.json")
 
         with open(default_config_path) as f:
             default_config = json.load(f)
 
-        #TODO: allow non-default configuration for ssl
+        config = serve.Config(override_ssl_env=self.ssl_env, **default_config)
 
-        local_config["external_host"] = self.options.get("external_host", None)
-        local_config["ssl"]["encrypt_after_connect"] = self.options.get("encrypt_after_connect", False)
+        config.ports = {
+            "http": [8000, 8001],
+            "https": [8443],
+            "ws": [8888]
+        }
 
-        config = serve.merge_json(default_config, local_config)
-        config["doc_root"] = serve_path(self.test_paths)
+        if os.path.exists(override_path):
+            with open(override_path) as f:
+                override_obj = json.load(f)
+            config.update(override_obj)
 
-        if not self.ssl_env.ssl_enabled:
-            config["ports"]["https"] = [None]
+        config.check_subdomains = False
+        config.ssl = {}
 
-        host = self.options.get("certificate_domain", config["host"])
-        hosts = [host]
-        hosts.extend("%s.%s" % (item[0], host) for item in serve.get_subdomains(host).values())
-        key_file, certificate = self.ssl_env.host_cert_path(hosts)
+        if "browser_host" in self.options:
+            config.browser_host = self.options["browser_host"]
 
-        config["key_file"] = key_file
-        config["certificate"] = certificate
+        if "bind_address" in self.options:
+            config.bind_address = self.options["bind_address"]
 
-        serve.set_computed_defaults(config)
+        config.server_host = self.options.get("server_host", None)
+        config.ssl["encrypt_after_connect"] = self.options.get("encrypt_after_connect", False)
+        config.doc_root = serve_path(self.test_paths)
 
         return config
 
@@ -200,10 +192,14 @@
         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}, "text/javascript",
+                 {"output": self.pause_after_test}, "text/javascript;charset=utf8",
                  "/resources/testharnessreport.js")]:
             path = os.path.normpath(os.path.join(here, path))
-            route_builder.add_static(path, format_args, content_type, route)
+            # 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)
 
         data = b""
         with open(os.path.join(repo_root, "resources", "testdriver.js"), "rb") as fp:
@@ -230,20 +226,23 @@
             if not failed:
                 return
             time.sleep(0.5)
-        raise EnvironmentError("Servers failed to start (scheme:port): %s" % ("%s:%s" for item in failed))
+        raise EnvironmentError("Servers failed to start: %s" %
+                               ", ".join("%s:%s" % item for item in failed))
 
     def test_servers(self):
         failed = []
+        host = self.config["server_host"]
         for scheme, servers in self.servers.iteritems():
             for port, server in servers:
                 if self.test_server_port:
                     s = socket.socket()
                     try:
-                        s.connect((self.config["host"], port))
+                        s.connect((host, port))
                     except socket.error:
-                        failed.append((scheme, port))
+                        failed.append((host, port))
                     finally:
                         s.close()
 
                 if not server.is_alive():
                     failed.append((scheme, port))
+        return failed
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/base.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/base.py
index 7178b2e..0942bc6 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/base.py
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/base.py
@@ -8,6 +8,7 @@
 from abc import ABCMeta, abstractmethod
 
 from ..testrunner import Stop
+from protocol import Protocol, BaseProtocolPart
 
 here = os.path.split(__file__)[0]
 
@@ -171,7 +172,7 @@
 
     def server_url(self, protocol):
         return "%s://%s:%s" % (protocol,
-                               self.server_config["host"],
+                               self.server_config["browser_host"],
                                self.server_config["ports"][protocol][0])
 
     def test_url(self, test):
@@ -192,13 +193,16 @@
         if hasattr(e, "status") and e.status in test.result_cls.statuses:
             status = e.status
         else:
-            status = "ERROR"
+            status = "INTERNAL-ERROR"
         message = unicode(getattr(e, "message", ""))
         if message:
             message += "\n"
         message += traceback.format_exc(e)
         return test.result_cls(status, message), []
 
+    def wait(self):
+        self.protocol.base.wait()
+
 
 class TestharnessExecutor(TestExecutor):
     convert_result = testharness_result_converter
@@ -367,25 +371,6 @@
         from . import pytestrunner
 
 
-class Protocol(object):
-    def __init__(self, executor, browser):
-        self.executor = executor
-        self.browser = browser
-
-    @property
-    def logger(self):
-        return self.executor.logger
-
-    def setup(self, runner):
-        pass
-
-    def teardown(self):
-        pass
-
-    def wait(self):
-        pass
-
-
 class WdspecRun(object):
     def __init__(self, func, session, path, timeout):
         self.func = func
@@ -421,14 +406,40 @@
             if message:
                 message += "\n"
             message += traceback.format_exc(e)
-            self.result = False, ("ERROR", message)
+            self.result = False, ("INTERNAL-ERROR", message)
         finally:
             self.result_flag.set()
 
 
+class ConnectionlessBaseProtocolPart(BaseProtocolPart):
+    def execute_script(self, script, async=False):
+        pass
+
+    def set_timeout(self, timeout):
+        pass
+
+    def wait(self):
+        pass
+
+    def set_window(self, handle):
+        pass
+
+
+class ConnectionlessProtocol(Protocol):
+    implements = [ConnectionlessBaseProtocolPart]
+
+    def connect(self):
+        pass
+
+    def after_connect(self):
+        pass
+
+
 class WebDriverProtocol(Protocol):
     server_cls = None
 
+    implements = [ConnectionlessBaseProtocolPart]
+
     def __init__(self, executor, browser):
         Protocol.__init__(self, executor, browser)
         self.webdriver_binary = executor.webdriver_binary
@@ -437,24 +448,21 @@
         self.session_config = None
         self.server = None
 
-    def setup(self, runner):
+    def connect(self):
         """Connect to browser via the HTTP server."""
-        try:
-            self.server = self.server_cls(
-                self.logger,
-                binary=self.webdriver_binary,
-                args=self.webdriver_args)
-            self.server.start(block=False)
-            self.logger.info(
-                "WebDriver HTTP server listening at %s" % self.server.url)
-            self.session_config = {"host": self.server.host,
-                                   "port": self.server.port,
-                                   "capabilities": self.capabilities}
-        except Exception:
-            self.logger.error(traceback.format_exc())
-            self.executor.runner.send_message("init_failed")
-        else:
-            self.executor.runner.send_message("init_succeeded")
+        self.server = self.server_cls(
+            self.logger,
+            binary=self.webdriver_binary,
+            args=self.webdriver_args)
+        self.server.start(block=False)
+        self.logger.info(
+            "WebDriver HTTP server listening at %s" % self.server.url)
+        self.session_config = {"host": self.server.host,
+                               "port": self.server.port,
+                               "capabilities": self.capabilities}
+
+    def after_connect(self):
+        pass
 
     def teardown(self):
         if self.server is not None and self.server.is_alive:
@@ -476,3 +484,96 @@
         conn.request("HEAD", self.server.base_path + "invalid")
         res = conn.getresponse()
         return res.status == 404
+
+
+class CallbackHandler(object):
+    """Handle callbacks from testdriver-using tests.
+
+    The default implementation here makes sense for things that are roughly like
+    WebDriver. Things that are more different to WebDriver may need to create a
+    fully custom implementation."""
+
+    def __init__(self, logger, protocol, test_window):
+        self.protocol = protocol
+        self.test_window = test_window
+        self.logger = logger
+        self.callbacks = {
+            "action": self.process_action,
+            "complete": self.process_complete
+        }
+
+        self.actions = {
+            "click": ClickAction(self.logger, self.protocol),
+            "send_keys": SendKeysAction(self.logger, self.protocol)
+        }
+
+    def __call__(self, result):
+        url, command, payload = result
+        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 = [url] + payload
+        return True, rv
+
+    def process_action(self, url, payload):
+        parent = self.protocol.base.current_window
+        try:
+            self.protocol.base.set_window(self.test_window)
+            action = payload["action"]
+            self.logger.debug("Got action: %s" % action)
+            try:
+                action_handler = self.actions[action]
+            except KeyError:
+                raise ValueError("Unknown action %s" % action)
+            try:
+                action_handler(payload)
+            except Exception as e:
+                self.logger.warning("Action %s failed" % action)
+                self.logger.warning(traceback.format_exc())
+                self._send_message("complete", "failure")
+            else:
+                self.logger.debug("Action %s completed" % action)
+                self._send_message("complete", "success")
+        finally:
+            self.protocol.base.set_window(parent)
+
+        return False, None
+
+    def _send_message(self, message_type, status, message=None):
+        self.protocol.testdriver.send_message(message_type, status, message=message)
+
+class ClickAction(object):
+    def __init__(self, logger, protocol):
+        self.logger = logger
+        self.protocol = protocol
+
+    def __call__(self, payload):
+        selector = payload["selector"]
+        elements = self.protocol.select.elements_by_selector(selector)
+        if len(elements) == 0:
+            raise ValueError("Selector matches no elements")
+        elif len(elements) > 1:
+            raise ValueError("Selector matches multiple elements")
+        self.logger.debug("Clicking element: %s" % selector)
+        self.protocol.click.element(elements[0])
+
+class SendKeysAction(object):
+    def __init__(self, logger, protocol):
+        self.logger = logger
+        self.protocol = protocol
+
+    def __call__(self, payload):
+        selector = payload["selector"]
+        keys = payload["keys"]
+        elements = self.protocol.select.elements_by_selector(selector)
+        if len(elements) == 0:
+            raise ValueError("Selector matches no elements")
+        elif len(elements) > 1:
+            raise ValueError("Selector matches multiple elements")
+        self.logger.debug("Sending keys to element: %s" % selector)
+        self.protocol.send_keys.send_keys(elements[0], keys)
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/executormarionette.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/executormarionette.py
index faf087b..d06ee3c 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/executormarionette.py
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/executormarionette.py
@@ -1,3 +1,4 @@
+import json
 import os
 import socket
 import threading
@@ -11,8 +12,8 @@
 
 here = os.path.join(os.path.split(__file__)[0])
 
-from .base import (ExecutorException,
-                   Protocol,
+from .base import (CallbackHandler,
+                   ExecutorException,
                    RefTestExecutor,
                    RefTestImplementation,
                    TestExecutor,
@@ -24,12 +25,19 @@
                    testharness_result_converter,
                    reftest_result_converter,
                    strip_server)
-
+from .protocol import (BaseProtocolPart,
+                       TestharnessProtocolPart,
+                       PrefsProtocolPart,
+                       Protocol,
+                       StorageProtocolPart,
+                       SelectorProtocolPart,
+                       ClickProtocolPart,
+                       SendKeysProtocolPart,
+                       TestDriverProtocolPart)
 from ..testrunner import Stop
 from ..webdriver_server import GeckoDriverServer
 
 
-
 def do_delayed_imports():
     global errors, marionette
 
@@ -42,78 +50,17 @@
         from marionette_driver import marionette, errors
 
 
-class MarionetteProtocol(Protocol):
-    def __init__(self, executor, browser, capabilities=None, timeout_multiplier=1):
-        do_delayed_imports()
-
-        Protocol.__init__(self, executor, browser)
-        self.marionette = None
-        self.marionette_port = browser.marionette_port
-        self.capabilities = capabilities
-        self.timeout_multiplier = timeout_multiplier
+class MarionetteBaseProtocolPart(BaseProtocolPart):
+    def __init__(self, parent):
+        super(MarionetteBaseProtocolPart, self).__init__(parent)
         self.timeout = None
-        self.runner_handle = None
 
-    def setup(self, runner):
-        """Connect to browser via Marionette."""
-        Protocol.setup(self, runner)
+    def setup(self):
+        self.marionette = self.parent.marionette
 
-        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='localhost',
-                                                port=self.marionette_port,
-                                                socket_timeout=None,
-                                                startup_timeout=startup_timeout)
-        try:
-            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.logger.debug("Marionette session started")
-
-        except Exception as e:
-            self.logger.warning("Failed to start a Marionette session: %s" % e)
-            self.executor.runner.send_message("init_failed")
-
-        else:
-            try:
-                self.after_connect()
-            except Exception:
-                self.logger.warning("Post-connection steps failed")
-                self.logger.error(traceback.format_exc())
-                self.executor.runner.send_message("init_failed")
-            else:
-                self.executor.runner.send_message("init_succeeded")
-
-    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
-
-    @property
-    def is_alive(self):
-        """Check if the Marionette connection is still active."""
-        try:
-            self.marionette.current_window_handle
-        except Exception:
-            return False
-        return True
-
-    def after_connect(self):
-        self.load_runner(self.executor.last_environment["protocol"])
+    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)
 
     def set_timeout(self, timeout):
         """Set the Marionette script timeout.
@@ -121,13 +68,60 @@
         :param timeout: Script timeout in seconds
 
         """
-        self.marionette.timeout.script = timeout
-        self.timeout = timeout
+        if timeout != self.timeout:
+            self.marionette.timeout.script = timeout
+            self.timeout = timeout
 
-    def load_runner(self, protocol):
+    @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 (socket.error, 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 (socket.timeout, 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
+
+    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
-        self.marionette.execute_script("if (window.wrappedJSObject.win) {window.wrappedJSObject.win.close()}")
-        url = urlparse.urljoin(self.executor.server_url(protocol), "/testharness_runner.html")
+        self.marionette.execute_script("if (window.win) {window.win.close()}")
+        url = urlparse.urljoin(self.parent.executor.server_url(url_protocol),
+                               "/testharness_runner.html")
         self.logger.debug("Loading %s" % url)
         self.runner_handle = self.marionette.current_window_handle
         try:
@@ -142,7 +136,7 @@
         self.marionette.execute_script(
             "document.title = '%s'" % threading.current_thread().name.replace("'", '"'))
 
-    def close_old_windows(self, protocol):
+    def close_old_windows(self, url_protocol):
         handles = self.marionette.window_handles
         runner_handle = None
         try:
@@ -168,7 +162,8 @@
 
         self.marionette.switch_to_window(runner_handle)
         if runner_handle != self.runner_handle:
-            self.load_runner(protocol)
+            self.load_runner(url_protocol)
+        return self.runner_handle
 
     def dismiss_alert(self, f):
         while True:
@@ -183,50 +178,36 @@
             else:
                 break
 
-    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:
+    def get_test_window(self, window_id, parent):
+        test_window = None
+        if window_id:
             try:
-                self.marionette.timeout.script = socket_timeout / 2
-            except (socket.error, IOError):
-                self.logger.debug("Socket closed")
-                return
-
-        self.marionette.switch_to_window(self.runner_handle)
-        while True:
-            try:
-                self.marionette.execute_async_script("")
-            except errors.NoSuchWindowException:
-                # The window closed
-                break
-            except errors.ScriptTimeoutException:
-                self.logger.debug("Script timed out")
+                # Try this, it's in Level 1 but nothing supports it yet
+                win_s = self.marionette.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
-            except (socket.timeout, IOError):
-                self.logger.debug("Socket closed")
-                break
-            except Exception as e:
-                self.logger.warning(traceback.format_exc(e))
-                break
 
-    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.clear_user_pref(name)
+        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:
-                self.set_pref(name, value)
+                raise Exception("unable to find test window")
 
-        for name, value in new_environment.get("prefs", {}).iteritems():
-            self.executor.original_pref_values[name] = self.get_pref(name)
-            self.set_pref(name, value)
+        assert test_window != parent
+        return test_window
 
-    def set_pref(self, name, value):
+
+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)
@@ -258,7 +239,7 @@
         with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
             self.marionette.execute_script(script)
 
-    def clear_user_pref(self, name):
+    def clear(self, name):
         self.logger.info("Clearing pref %s" % (name))
         script = """
             let prefInterface = Components.classes["@mozilla.org/preferences-service;1"]
@@ -269,7 +250,7 @@
         with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
             self.marionette.execute_script(script)
 
-    def get_pref(self, name):
+    def get(self, name):
         script = """
             let prefInterface = Components.classes["@mozilla.org/preferences-service;1"]
                                           .getService(Components.interfaces.nsIPrefBranch);
@@ -289,6 +270,11 @@
         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 = """
@@ -307,12 +293,125 @@
             self.marionette.execute_script(script)
 
 
+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 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.marionette.execute_script("window.postMessage(%s, '*')" % json.dumps(obj))
+
+
+class MarionetteProtocol(Protocol):
+    implements = [MarionetteBaseProtocolPart,
+                  MarionetteTestharnessProtocolPart,
+                  MarionettePrefsProtocolPart,
+                  MarionetteStorageProtocolPart,
+                  MarionetteSelectorProtocolPart,
+                  MarionetteClickProtocolPart,
+                  MarionetteSendKeysProtocolPart,
+                  MarionetteTestDriverProtocolPart]
+
+    def __init__(self, executor, browser, capabilities=None, timeout_multiplier=1):
+        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
+
+    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='localhost',
+                                                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.logger.debug("Marionette session started")
+
+    def after_connect(self):
+        self.testharness.load_runner(self.executor.last_environment["protocol"])
+
+    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.marionette = protocol.marionette
         self.func = func
         self.url = url
         self.timeout = timeout
@@ -322,19 +421,18 @@
         index = self.url.rfind("/storage/")
         if index != -1:
             # Clear storage
-            self.protocol.clear_origin(self.url)
+            self.protocol.storage.clear_origin(self.url)
 
         timeout = self.timeout
 
         try:
             if timeout is not None:
-                if timeout + extra_timeout != self.protocol.timeout:
-                    self.protocol.set_timeout(timeout + extra_timeout)
+                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.set_timeout(2**28 - 1)
+                self.protocol.base.set_timeout(2**28 - 1)
         except IOError:
             self.logger.error("Lost marionette connection before starting test")
             return Stop
@@ -356,14 +454,14 @@
             # 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, ("ERROR", None)
+                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.marionette, self.url, self.timeout)
+            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)
@@ -377,13 +475,15 @@
             if message:
                 message += "\n"
             message += traceback.format_exc(e)
-            self.result = False, ("ERROR", e)
+            self.result = False, ("INTERNAL-ERROR", e)
 
         finally:
             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,
                  **kwargs):
@@ -394,6 +494,7 @@
 
         self.protocol = MarionetteProtocol(self, browser, capabilities, timeout_multiplier)
         self.script = open(os.path.join(here, "testharness_marionette.js")).read()
+        self.script_resume = open(os.path.join(here, "testharness_marionette_resume.js")).read()
         self.close_after_done = close_after_done
         self.window_id = str(uuid.uuid4())
 
@@ -409,7 +510,7 @@
         self.protocol.on_environment_change(self.last_environment, new_environment)
 
         if new_environment["protocol"] != self.last_environment["protocol"]:
-            self.protocol.load_runner(new_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
@@ -425,24 +526,34 @@
 
         return (test.result_cls(*data), [])
 
-    def do_testharness(self, marionette, url, timeout):
-        if self.close_after_done:
-            marionette.execute_script("if (window.wrappedJSObject.win) {window.wrappedJSObject.win.close()}")
-            self.protocol.close_old_windows(self.protocol)
+    def do_testharness(self, protocol, url, timeout):
+        protocol.base.execute_script("if (window.win) {window.win.close()}")
+        parent_window = protocol.testharness.close_old_windows(protocol)
 
         if timeout is not None:
             timeout_ms = str(timeout * 1000)
         else:
             timeout_ms = "null"
 
-        script = self.script % {"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}
+        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}
 
-        rv = marionette.execute_async_script(script, new_sandbox=False)
+        script = self.script % format_map
+
+        rv = protocol.base.execute_script(script)
+        test_window = protocol.testharness.get_test_window(self.window_id, parent_window)
+
+        handler = CallbackHandler(self.logger, protocol, test_window)
+        while True:
+            result = protocol.base.execute_script(
+                self.script_resume % format_map, async=True)
+            done, rv = handler(result)
+            if done:
+                break
         return rv
 
 
@@ -506,8 +617,8 @@
                 self.has_window = False
 
             if not self.has_window:
-                self.protocol.marionette.execute_script(self.script)
-                self.protocol.marionette.switch_to_window(self.protocol.marionette.window_handles[-1])
+                self.protocol.base.execute_script(self.script)
+                self.protocol.base.set_window(self.protocol.marionette.window_handles[-1])
                 self.has_window = True
 
         result = self.implementation.run_test(test)
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/executorselenium.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/executorselenium.py
index f905fac..98c49bd 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/executorselenium.py
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/executorselenium.py
@@ -8,12 +8,19 @@
 import urlparse
 import uuid
 
-from .base import (Protocol,
+from .base import (CallbackHandler,
                    RefTestExecutor,
                    RefTestImplementation,
                    TestharnessExecutor,
                    extra_timeout,
                    strip_server)
+from .protocol import (BaseProtocolPart,
+                       TestharnessProtocolPart,
+                       Protocol,
+                       SelectorProtocolPart,
+                       ClickProtocolPart,
+                       SendKeysProtocolPart,
+                       TestDriverProtocolPart)
 from ..testrunner import Stop
 
 here = os.path.join(os.path.split(__file__)[0])
@@ -32,45 +39,150 @@
     from selenium.webdriver.remote.remote_connection import RemoteConnection
 
 
+class SeleniumBaseProtocolPart(BaseProtocolPart):
+    def setup(self):
+        self.webdriver = self.parent.webdriver
+
+    def execute_script(self, script, async=False):
+        method = self.webdriver.execute_async_script if async else self.webdriver.execute_script
+        return method(script)
+
+    def set_timeout(self, timeout):
+        self.webdriver.set_script_timeout(timeout * 1000)
+
+    @property
+    def current_window(self):
+        return self.webdriver.current_window_handle
+
+    def set_window(self, handle):
+        self.webdriver.switch_to_window(handle)
+
+    def wait(self):
+        while True:
+            try:
+                self.webdriver.execute_async_script("")
+            except exceptions.TimeoutException:
+                pass
+            except (socket.timeout, exceptions.NoSuchWindowException,
+                    exceptions.ErrorInResponseException, IOError):
+                break
+            except Exception as e:
+                self.logger.error(traceback.format_exc(e))
+                break
+
+
+class SeleniumTestharnessProtocolPart(TestharnessProtocolPart):
+    def setup(self):
+        self.webdriver = self.parent.webdriver
+
+    def load_runner(self, url_protocol):
+        url = urlparse.urljoin(self.parent.executor.server_url(url_protocol),
+                               "/testharness_runner.html")
+        self.logger.debug("Loading %s" % url)
+        self.webdriver.get(url)
+        self.webdriver.execute_script("document.title = '%s'" %
+                                      threading.current_thread().name.replace("'", '"'))
+
+    def close_old_windows(self):
+        exclude = self.webdriver.current_window_handle
+        handles = [item for item in self.webdriver.window_handles if item != exclude]
+        for handle in handles:
+            try:
+                self.webdriver.switch_to_window(handle)
+                self.webdriver.close()
+            except exceptions.NoSuchWindowException:
+                pass
+        self.webdriver.switch_to_window(exclude)
+        return exclude
+
+    def get_test_window(self, window_id, parent):
+        test_window = None
+        if window_id:
+            try:
+                # Try this, it's in Level 1 but nothing supports it yet
+                win_s = self.webdriver.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:
+            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")
+
+        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)
+
+
+class SeleniumClickProtocolPart(ClickProtocolPart):
+    def setup(self):
+        self.webdriver = self.parent.webdriver
+
+    def element(self, element):
+        return element.click()
+
+class SeleniumSendKeysProtocolPart(SendKeysProtocolPart):
+    def setup(self):
+        self.webdriver = self.parent.webdriver
+
+    def send_keys(self, element, keys):
+        return element.send_keys(keys)
+
+
+class SeleniumTestDriverProtocolPart(TestDriverProtocolPart):
+    def setup(self):
+        self.webdriver = self.parent.webdriver
+
+    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.webdriver.execute_script("window.postMessage(%s, '*')" % json.dumps(obj))
+
+
 class SeleniumProtocol(Protocol):
+    implements = [SeleniumBaseProtocolPart,
+                  SeleniumTestharnessProtocolPart,
+                  SeleniumSelectorProtocolPart,
+                  SeleniumClickProtocolPart,
+                  SeleniumSendKeysProtocolPart,
+                  SeleniumTestDriverProtocolPart]
+
     def __init__(self, executor, browser, capabilities, **kwargs):
         do_delayed_imports()
 
-        Protocol.__init__(self, executor, browser)
+        super(SeleniumProtocol, self).__init__(executor, browser)
         self.capabilities = capabilities
         self.url = browser.webdriver_url
         self.webdriver = None
 
-    def setup(self, runner):
+    def connect(self):
         """Connect to browser via Selenium's WebDriver implementation."""
-        self.runner = runner
         self.logger.debug("Connecting to Selenium on URL: %s" % self.url)
 
-        session_started = False
-        try:
-            self.webdriver = webdriver.Remote(command_executor=RemoteConnection(self.url.strip("/"),
-                                                                                resolve_ip=False),
-                                              desired_capabilities=self.capabilities)
-        except Exception:
-            self.logger.warning(
-                "Connecting to Selenium failed:\n%s" % traceback.format_exc())
-        else:
-            self.logger.debug("Selenium session started")
-            session_started = True
+        self.webdriver = webdriver.Remote(command_executor=RemoteConnection(self.url.strip("/"),
+                                                                            resolve_ip=False),
+                                          desired_capabilities=self.capabilities)
 
-        if not session_started:
-            self.logger.warning("Failed to connect to Selenium")
-            self.executor.runner.send_message("init_failed")
-        else:
-            try:
-                self.after_connect()
-            except Exception:
-                print >> sys.stderr, traceback.format_exc()
-                self.logger.warning(
-                    "Failed to connect to navigate initial page")
-                self.executor.runner.send_message("init_failed")
-            else:
-                self.executor.runner.send_message("init_succeeded")
+    def after_conect(self):
+        pass
 
     def teardown(self):
         self.logger.debug("Hanging up on Selenium session")
@@ -90,35 +202,14 @@
         return True
 
     def after_connect(self):
-        self.load_runner("http")
-
-    def load_runner(self, protocol):
-        url = urlparse.urljoin(self.executor.server_url(protocol),
-                               "/testharness_runner.html")
-        self.logger.debug("Loading %s" % url)
-        self.webdriver.get(url)
-        self.webdriver.execute_script("document.title = '%s'" %
-                                      threading.current_thread().name.replace("'", '"'))
-
-    def wait(self):
-        while True:
-            try:
-                self.webdriver.execute_async_script("")
-            except exceptions.TimeoutException:
-                pass
-            except (socket.timeout, exceptions.NoSuchWindowException,
-                    exceptions.ErrorInResponseException, IOError):
-                break
-            except Exception as e:
-                self.logger.error(traceback.format_exc(e))
-                break
+        self.testharness.load_runner(self.executor.last_environment["protocol"])
 
 
 class SeleniumRun(object):
-    def __init__(self, func, webdriver, url, timeout):
+    def __init__(self, func, protocol, url, timeout):
         self.func = func
         self.result = None
-        self.webdriver = webdriver
+        self.protocol = protocol
         self.url = url
         self.timeout = timeout
         self.result_flag = threading.Event()
@@ -127,7 +218,7 @@
         timeout = self.timeout
 
         try:
-            self.webdriver.set_script_timeout((timeout + extra_timeout) * 1000)
+            self.protocol.base.set_timeout((timeout + extra_timeout))
         except exceptions.ErrorInResponseException:
             self.logger.error("Lost WebDriver connection")
             return Stop
@@ -144,7 +235,7 @@
 
     def _run(self):
         try:
-            self.result = True, self.func(self.webdriver, self.url, self.timeout)
+            self.result = True, self.func(self.protocol, self.url, self.timeout)
         except exceptions.TimeoutException:
             self.result = False, ("EXTERNAL-TIMEOUT", None)
         except (socket.timeout, exceptions.ErrorInResponseException):
@@ -154,7 +245,7 @@
             if message:
                 message += "\n"
             message += traceback.format_exc(e)
-            self.result = False, ("ERROR", e)
+            self.result = False, ("INTERNAL-ERROR", e)
         finally:
             self.result_flag.set()
 
@@ -182,13 +273,13 @@
 
     def on_environment_change(self, new_environment):
         if new_environment["protocol"] != self.last_environment["protocol"]:
-            self.protocol.load_runner(new_environment["protocol"])
+            self.protocol.testharness.load_runner(new_environment["protocol"])
 
     def do_test(self, test):
         url = self.test_url(test)
 
         success, data = SeleniumRun(self.do_testharness,
-                                    self.protocol.webdriver,
+                                    self.protocol,
                                     url,
                                     test.timeout * self.timeout_multiplier).run()
 
@@ -197,111 +288,28 @@
 
         return (test.result_cls(*data), [])
 
-    def do_testharness(self, webdriver, url, timeout):
+    def do_testharness(self, protocol, url, timeout):
         format_map = {"abs_url": url,
                       "url": strip_server(url),
                       "window_id": self.window_id,
                       "timeout_multiplier": self.timeout_multiplier,
                       "timeout": timeout * 1000}
 
-        parent = webdriver.current_window_handle
-        handles = [item for item in webdriver.window_handles if item != parent]
-        for handle in handles:
-            try:
-                webdriver.switch_to_window(handle)
-                webdriver.close()
-            except exceptions.NoSuchWindowException:
-                pass
-        webdriver.switch_to_window(parent)
+        parent_window = protocol.testharness.close_old_windows()
+        # Now start the test harness
+        protocol.base.execute_script(self.script % format_map)
+        test_window = protocol.testharness.get_test_window(webdriver, parent_window)
 
-        webdriver.execute_script(self.script % format_map)
-        try:
-            # Try this, it's in Level 1 but nothing supports it yet
-            win_s = webdriver.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:
-            after = 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")
-        assert test_window != parent
-
-        handler = CallbackHandler(webdriver, test_window, self.logger)
+        handler = CallbackHandler(self.logger, protocol, test_window)
         while True:
-            result = webdriver.execute_async_script(
-                self.script_resume % format_map)
+            result = protocol.base.execute_script(
+                self.script_resume % format_map, async=True)
             done, rv = handler(result)
             if done:
                 break
         return rv
 
 
-class CallbackHandler(object):
-    def __init__(self, webdriver, test_window, logger):
-        self.webdriver = webdriver
-        self.test_window = test_window
-        self.logger = logger
-
-    def __call__(self, result):
-        self.logger.debug("Got async callback: %s" % result[1])
-        try:
-            attr = getattr(self, "process_%s" % result[1])
-        except AttributeError:
-            raise ValueError("Unknown callback type %r" % result[1])
-        else:
-            return attr(result)
-
-    def process_complete(self, result):
-        rv = [result[0]] + result[2]
-        return True, rv
-
-    def process_action(self, result):
-        parent = self.webdriver.current_window_handle
-        try:
-            self.webdriver.switch_to.window(self.test_window)
-            action = result[2]["action"]
-            self.logger.debug("Got action: %s" % action)
-            if action == "click":
-                selector = result[2]["selector"]
-                elements = self.webdriver.find_elements_by_css_selector(selector)
-                if len(elements) == 0:
-                    raise ValueError("Selector matches no elements")
-                elif len(elements) > 1:
-                    raise ValueError("Selector matches multiple elements")
-                self.logger.debug("Clicking element: %s" % selector)
-                try:
-                    elements[0].click()
-                except (exceptions.ElementNotInteractableException,
-                        exceptions.ElementNotVisibleException) as e:
-                    self._send_message("complete",
-                                       "failure",
-                                       e)
-                    self.logger.debug("Clicking element failed: %s" % str(e))
-                else:
-                    self._send_message("complete",
-                                       "success")
-                    self.logger.debug("Clicking element succeeded")
-        finally:
-            self.webdriver.switch_to.window(parent)
-
-        return False, None
-
-    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.webdriver.execute_script("window.postMessage(%s, '*')" % json.dumps(obj))
-
-
-
 class SeleniumRefTestExecutor(RefTestExecutor):
     def __init__(self, browser, server_config, timeout_multiplier=1,
                  screenshot_cache=None, close_after_done=True,
@@ -342,11 +350,12 @@
         assert dpi is None
 
         return SeleniumRun(self._screenshot,
-                           self.protocol.webdriver,
+                           self.protocol,
                            self.test_url(test),
                            test.timeout).run()
 
-    def _screenshot(self, webdriver, url, timeout):
+    def _screenshot(self, protocol, url, timeout):
+        webdriver = protocol.webdriver
         webdriver.get(url)
 
         webdriver.execute_async_script(self.wait_script)
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/executorservo.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/executorservo.py
index edacddd..fb79cf2 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/executorservo.py
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/executorservo.py
@@ -13,14 +13,15 @@
 
 from mozprocess import ProcessHandler
 
-from tools.serve.serve import make_hosts_file
+from serve.serve import make_hosts_file
 
 from .base import (ExecutorException,
-                   Protocol,
+                   ConnectionlessProtocol,
                    RefTestImplementation,
                    testharness_result_converter,
                    reftest_result_converter,
-                   WdspecExecutor, WebDriverProtocol)
+                   WdspecExecutor,
+                   WebDriverProtocol)
 from .process import ProcessTestExecutor
 from ..browsers.base import browser_command
 from ..wpttest import WdspecResult, WdspecSubtestResult
@@ -50,7 +51,7 @@
         self.pause_after_test = pause_after_test
         self.result_data = None
         self.result_flag = None
-        self.protocol = Protocol(self, browser)
+        self.protocol = ConnectionlessProtocol(self, browser)
         self.hosts_path = write_hosts_file(server_config)
 
     def teardown(self):
@@ -181,7 +182,7 @@
                                      timeout_multiplier=timeout_multiplier,
                                      debug_info=debug_info)
 
-        self.protocol = Protocol(self, browser)
+        self.protocol = ConnectionlessProtocol(self, browser)
         self.screenshot_cache = screenshot_cache
         self.implementation = RefTestImplementation(self)
         self.tempdir = tempfile.mkdtemp()
@@ -284,5 +285,6 @@
 class ServoDriverProtocol(WebDriverProtocol):
     server_cls = ServoDriverServer
 
+
 class ServoWdspecExecutor(WdspecExecutor):
     protocol_cls = ServoDriverProtocol
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/executorservodriver.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/executorservodriver.py
index ed7afaf..626c987 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/executorservodriver.py
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/executorservodriver.py
@@ -33,28 +33,15 @@
         self.port = browser.webdriver_port
         self.session = None
 
-    def setup(self, runner):
+    def connect(self):
         """Connect to browser via WebDriver."""
-        self.runner = runner
-
         url = "http://%s:%d" % (self.host, self.port)
-        session_started = False
-        try:
-            self.session = webdriver.Session(self.host, self.port,
-                extension=webdriver.servo.ServoCommandExtensions)
-            self.session.start()
-        except Exception:
-            self.logger.warning(
-                "Connecting with WebDriver failed:\n%s" % traceback.format_exc())
-        else:
-            self.logger.debug("session started")
-            session_started = True
+        self.session = webdriver.Session(self.host, self.port,
+                                         extension=webdriver.servo.ServoCommandExtensions)
+        self.session.start()
 
-        if not session_started:
-            self.logger.warning("Failed to connect via WebDriver")
-            self.executor.runner.send_message("init_failed")
-        else:
-            self.executor.runner.send_message("init_succeeded")
+    def after_connect(self):
+        pass
 
     def teardown(self):
         self.logger.debug("Hanging up on WebDriver session")
@@ -72,9 +59,6 @@
             return False
         return True
 
-    def after_connect(self):
-        pass
-
     def wait(self):
         while True:
             try:
@@ -125,7 +109,7 @@
             if message:
                 message += "\n"
             message += traceback.format_exc(e)
-            self.result = False, ("ERROR", e)
+            self.result = False, ("INTERNAL-ERROR", e)
         finally:
             self.result_flag.set()
 
@@ -230,7 +214,7 @@
             if message:
                 message += "\n"
             message += traceback.format_exc(e)
-            return test.result_cls("ERROR", message), []
+            return test.result_cls("INTERNAL-ERROR", message), []
 
     def screenshot(self, test, viewport_size, dpi):
         # https://github.com/w3c/wptrunner/issues/166
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/executorwebkit.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/executorwebkit.py
new file mode 100644
index 0000000..c728ae1
--- /dev/null
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/executorwebkit.py
@@ -0,0 +1,10 @@
+from ..webdriver_server import WebKitDriverServer
+from .base import WdspecExecutor, WebDriverProtocol
+
+
+class WebKitDriverProtocol(WebDriverProtocol):
+    server_cls = WebKitDriverServer
+
+
+class WebKitDriverWdspecExecutor(WdspecExecutor):
+    protocol_cls = WebKitDriverProtocol
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/protocol.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/protocol.py
new file mode 100644
index 0000000..3c938f0
--- /dev/null
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/protocol.py
@@ -0,0 +1,292 @@
+import traceback
+from abc import ABCMeta, abstractmethod
+
+
+class Protocol(object):
+    """Backend for a specific browser-control protocol.
+
+    Each Protocol is composed of a set of ProtocolParts that implement
+    the APIs required for specific interactions. This reflects the fact
+    that not all implementaions will support exactly the same feature set.
+    Each ProtocolPart is exposed directly on the protocol through an accessor
+    attribute with a name given by its `name` property.
+
+    :param Executor executor: The Executor instance that's using this Protocol
+    :param Browser browser: The Browser using this protocol"""
+    __metaclass__ = ABCMeta
+
+    implements = []
+
+    def __init__(self, executor, browser):
+        self.executor = executor
+        self.browser = browser
+
+        for cls in self.implements:
+            name = cls.name
+            assert not hasattr(self, name)
+            setattr(self, name, cls(self))
+
+    @property
+    def logger(self):
+        """:returns: Current logger"""
+        return self.executor.logger
+
+    @property
+    def is_alive(self):
+        """Is the browser connection still active
+
+        :returns: A boolean indicating whether the connection is still active."""
+        return True
+
+    def setup(self, runner):
+        """Handle protocol setup, and send a message to the runner to indicate
+        success or failure."""
+        msg = None
+        try:
+            msg = "Failed to start protocol connection"
+            self.connect()
+
+            msg = None
+
+            for cls in self.implements:
+                getattr(self, cls.name).setup()
+
+            msg = "Post-connection steps failed"
+            self.after_connect()
+        except Exception:
+            if msg is not None:
+                self.logger.warning(msg)
+            self.logger.error(traceback.format_exc())
+            self.executor.runner.send_message("init_failed")
+            return
+        else:
+            self.executor.runner.send_message("init_succeeded")
+
+    @abstractmethod
+    def connect(self):
+        """Make a connection to the remote browser"""
+        pass
+
+    @abstractmethod
+    def after_connect(self):
+        """Run any post-connection steps. This happens after the ProtocolParts are
+        initalized so can depend on a fully-populated object."""
+        pass
+
+    def teardown(self):
+        """Run cleanup steps after the tests are finished."""
+        for cls in self.implements:
+            getattr(self, cls.name).teardown()
+
+
+class ProtocolPart(object):
+    """Base class  for all ProtocolParts.
+
+    :param Protocol parent: The parent protocol"""
+    __metaclass__ = ABCMeta
+
+    name = None
+
+    def __init__(self, parent):
+        self.parent = parent
+
+    @property
+    def logger(self):
+        """:returns: Current logger"""
+        return self.parent.logger
+
+    def setup(self):
+        """Run any setup steps required for the ProtocolPart."""
+        pass
+
+    def teardown(self):
+        """Run any teardown steps required for the ProtocolPart."""
+        pass
+
+
+class BaseProtocolPart(ProtocolPart):
+    """Generic bits of protocol that are required for multiple test types"""
+    __metaclass__ = ABCMeta
+
+    name = "base"
+
+    @abstractmethod
+    def execute_script(self, script, async=False):
+        """Execute javascript in the current Window.
+
+        :param str script: The js source to execute. This is implicitly wrapped in a function.
+        :param bool async: Whether the script is asynchronous in the webdriver
+                           sense i.e. whether the return value is the result of
+                           the initial function call or if it waits for some callback.
+        :returns: The result of the script execution.
+        """
+        pass
+
+    @abstractmethod
+    def set_timeout(self, timeout):
+        """Set the timeout for script execution.
+
+        :param timeout: Script timeout in seconds"""
+        pass
+
+    @abstractmethod
+    def wait(self):
+        """Wait indefinitely for the browser to close"""
+        pass
+
+    @property
+    def current_window(self):
+        """Return a handle identifying the current top level browsing context
+
+        :returns: A protocol-specific handle"""
+        pass
+
+    @abstractmethod
+    def set_window(self, handle):
+        """Set the top level browsing context to one specified by a given handle.
+
+        :param handle: A protocol-specific handle identifying a top level browsing
+                       context."""
+        pass
+
+
+class TestharnessProtocolPart(ProtocolPart):
+    """Protocol part required to run testharness tests."""
+    __metaclass__ = ABCMeta
+
+    name = "testharness"
+
+    @abstractmethod
+    def load_runner(self, url_protocol):
+        """Load the initial page used to control the tests.
+
+        :param str url_protocol: "https" or "http" depending on the test metadata.
+        """
+        pass
+
+    @abstractmethod
+    def close_old_windows(self, url_protocol):
+        """Close existing windows except for the initial runner window.
+        After calling this method there must be exactly one open window that
+        contains the initial runner page.
+
+        :param str url_protocol: "https" or "http" depending on the test metadata.
+        """
+        pass
+
+    @abstractmethod
+    def get_test_window(self, window_id, parent):
+        """Get the window handle dorresponding to the window containing the
+        currently active test.
+
+        :param window_id: A string containing the DOM name of the Window that
+        contains the test, or None.
+        :param parent: The handle of the runner window.
+        :returns: A protocol-specific window handle.
+        """
+        pass
+
+
+class PrefsProtocolPart(ProtocolPart):
+    """Protocol part that allows getting and setting browser prefs."""
+    __metaclass__ = ABCMeta
+
+    name = "prefs"
+
+    @abstractmethod
+    def set(self, name, value):
+        """Set the named pref to value.
+
+        :param name: A pref name of browser-specific type
+        :param value: A pref value of browser-specific type"""
+        pass
+
+    @abstractmethod
+    def get(self, name):
+        """Get the current value of a named pref
+
+        :param name: A pref name of browser-specific type
+        :returns: A pref value of browser-specific type"""
+        pass
+
+    @abstractmethod
+    def clear(self, name):
+        """Reset the value of a named pref back to the default.
+
+        :param name: A pref name of browser-specific type"""
+        pass
+
+
+class StorageProtocolPart(ProtocolPart):
+    """Protocol part for manipulating browser storage."""
+    __metaclass__ = ABCMeta
+
+    name = "storage"
+
+    @abstractmethod
+    def clear_origin(self, url):
+        """Clear all the storage for a specified origin.
+
+        :param url: A url belonging to the origin"""
+        pass
+
+
+class SelectorProtocolPart(ProtocolPart):
+    """Protocol part for selecting elements on the page."""
+    __metaclass__ = ABCMeta
+
+    name = "select"
+
+    @abstractmethod
+    def elements_by_selector(self, selector):
+        """Select elements matching a CSS selector
+
+        :param str selector: The CSS selector
+        :returns: A list of protocol-specific handles to elements"""
+        pass
+
+
+class ClickProtocolPart(ProtocolPart):
+    """Protocol part for performing trusted clicks"""
+    __metaclass__ = ABCMeta
+
+    name = "click"
+
+    @abstractmethod
+    def element(self, element):
+        """Perform a trusted click somewhere on a specific element.
+
+        :param element: A protocol-specific handle to an element."""
+        pass
+
+class SendKeysProtocolPart(ProtocolPart):
+    """Protocol part for performing trusted clicks"""
+    __metaclass__ = ABCMeta
+
+    name = "send_keys"
+
+    @abstractmethod
+    def send_keys(self, element, keys):
+        """Send keys to a specific element.
+
+        :param element: A protocol-specific handle to an element.
+        :param keys: A protocol-specific handle to a string of input keys."""
+        pass
+
+
+class TestDriverProtocolPart(ProtocolPart):
+    """Protocol part that implements the basic functionality required for
+    all testdriver-based tests."""
+    __metaclass__ = ABCMeta
+
+    name = "testdriver"
+
+    @abstractmethod
+    def send_message(self, message_type, status, message=None):
+        """Send a testdriver message to the browser.
+
+        :param str message_type: The kind of the message.
+        :param str status: Either "failure" or "success" depending on whether the
+                           previous command succeeded.
+        :param str message: Additional data to add to the message."""
+        pass
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/pytestrunner/runner.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/pytestrunner/runner.py
index 8eabf27..c1936eb 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/pytestrunner/runner.py
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/pytestrunner/runner.py
@@ -44,7 +44,7 @@
     os.environ["WD_HOST"] = session_config["host"]
     os.environ["WD_PORT"] = str(session_config["port"])
     os.environ["WD_CAPABILITIES"] = json.dumps(session_config["capabilities"])
-    os.environ["WD_SERVER_CONFIG"] = json.dumps(server_config)
+    os.environ["WD_SERVER_CONFIG"] = json.dumps(server_config.as_dict())
 
     harness = HarnessResultRecorder()
     subtests = SubtestResultRecorder()
@@ -62,7 +62,7 @@
                          path],
                         plugins=[harness, subtests])
         except Exception as e:
-            harness.outcome = ("ERROR", str(e))
+            harness.outcome = ("INTERNAL-ERROR", str(e))
 
     return (harness.outcome, subtests.results)
 
@@ -102,7 +102,19 @@
         self.record(report.nodeid, "PASS")
 
     def record_fail(self, report):
-        self.record(report.nodeid, "FAIL", stack=report.longrepr)
+        # pytest outputs the stacktrace followed by an error message prefixed
+        # with "E   ", e.g.
+        #
+        #        def test_example():
+        #  >         assert "fuu" in "foobar"
+        #  > E       AssertionError: assert 'fuu' in 'foobar'
+        message = ""
+        for line in report.longreprtext.splitlines():
+            if line.startswith("E   "):
+                message = line[1:].strip()
+                break
+
+        self.record(report.nodeid, "FAIL", message=message, stack=report.longrepr)
 
     def record_error(self, report):
         # error in setup/teardown
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/testharness_marionette.js b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/testharness_marionette.js
index e2b70a0..049caa4 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/testharness_marionette.js
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/testharness_marionette.js
@@ -1,31 +1,24 @@
-window.wrappedJSObject.timeout_multiplier = %(timeout_multiplier)d;
-window.wrappedJSObject.explicit_timeout = %(explicit_timeout)d;
+window.timeout_multiplier = %(timeout_multiplier)d;
+window.explicit_timeout = %(explicit_timeout)d;
 
-window.wrappedJSObject.addEventListener("message", function listener(event) {
-    if (event.data.type != "complete") {
-        return;
-    }
-    window.wrappedJSObject.removeEventListener("message", listener);
-    clearTimeout(timer);
-    var tests = event.data.tests;
-    var status = event.data.status;
+window.message_queue = [];
 
-    var subtest_results = tests.map(function (x) {
-        return [x.name, x.status, x.message, x.stack]
-    });
+window.setMessageListener = function(func) {
+  window.current_listener = func;
+  window.addEventListener(
+    "message",
+    func,
+    false
+  );
+};
 
-    marionetteScriptFinished(["%(url)s",
-                              status.status,
-                              status.message,
-                              status.stack,
-                              subtest_results]);
-}, false);
+window.setMessageListener(function(event) {
+  window.message_queue.push(event);
+});
 
-window.wrappedJSObject.win = window.open("%(abs_url)s", "%(window_id)s");
+window.win = window.open("%(abs_url)s", "%(window_id)s");
 
-var timer = null;
-if (%(timeout)s) {
-    timer = setTimeout(function() {
-        window.wrappedJSObject.win.timeout();
-    }, %(timeout)s);
-}
+window.timer = setTimeout(function() {
+  window.win.timeout();
+  window.win.close();
+}, %(timeout)s);
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/testharness_marionette_resume.js b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/testharness_marionette_resume.js
new file mode 100644
index 0000000..7a2df98
--- /dev/null
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/testharness_marionette_resume.js
@@ -0,0 +1,46 @@
+var callback = arguments[arguments.length - 1];
+
+function process_event(event) {
+  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":
+    window.setMessageListener(function(event) {
+      window.message_queue.push(event);
+    });
+    payload = data;
+    break;
+  default:
+    return;
+  }
+
+  callback(["%(url)s", data.type, payload]);
+}
+
+window.removeEventListener("message", window.current_listener);
+if (window.message_queue.length) {
+  var next = window.message_queue.shift();
+  process_event(next);
+} else {
+  window.addEventListener(
+    "message", function f(event) {
+      window.removeEventListener("message", f);
+      process_event(event);
+    }, false);
+}
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/testharness_webdriver_resume.js b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/testharness_webdriver_resume.js
index 7229777..7a2df98 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/testharness_webdriver_resume.js
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/executors/testharness_webdriver_resume.js
@@ -26,6 +26,8 @@
     });
     payload = data;
     break;
+  default:
+    return;
   }
 
   callback(["%(url)s", data.type, payload]);
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/testdriver-extra.js b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/testdriver-extra.js
index 856a33e..ef962d3 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/testdriver-extra.js
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/testdriver-extra.js
@@ -60,4 +60,14 @@
         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.opener.postMessage({"type": "action", "action": "send_keys", "selector": selector, "keys": keys}, "*");
+        return pending_promise;
+    };
 })();
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/testloader.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/testloader.py
index 199ed6c..9ac2e94 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/testloader.py
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/testloader.py
@@ -376,6 +376,7 @@
             if test.tags & self.tags:
                 yield test
 
+
 class ManifestLoader(object):
     def __init__(self, test_paths, force_manifest_update=False, manifest_download=False):
         do_delayed_imports()
@@ -430,8 +431,7 @@
 
         manifest.write(manifest_file, manifest_path)
 
-    def load_manifest(self, tests_path, metadata_path, url_base="/"):
-        manifest_path = os.path.join(metadata_path, "MANIFEST.json")
+    def load_manifest(self, tests_path, manifest_path, url_base="/", **kwargs):
         if (not os.path.exists(manifest_path) or
             self.force_manifest_update):
             self.update_manifest(manifest_path, tests_path, url_base, download=self.manifest_download)
@@ -444,6 +444,7 @@
 
         return manifest_file
 
+
 def iterfilter(filters, iter):
     for f in filters:
         iter = f(iter)
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/testrunner.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/testrunner.py
index 16623f7..e83bff8 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/testrunner.py
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/testrunner.py
@@ -108,7 +108,7 @@
             raise
 
     def wait(self):
-        self.executor.protocol.wait()
+        self.executor.wait()
         self.send_message("wait_finished")
 
     def send_message(self, command, *args):
@@ -304,6 +304,7 @@
         # This is started in the actual new thread
         self.logger = None
 
+        self.test_count = 0
         self.unexpected_count = 0
 
         # This may not really be what we want
@@ -559,16 +560,22 @@
                                     expected=expected,
                                     stack=result.stack)
 
-        # TODO: consider changing result if there is a crash dump file
-
-        # Write the result of the test harness
+        # We have a couple of status codes that are used internally, but not exposed to the
+        # user. These are used to indicate that some possibly-broken state was reached
+        # and we should restart the runner before the next test.
+        # INTERNAL-ERROR indicates a Python exception was caught in the harness
+        # EXTERNAL-TIMEOUT indicates we had to forcibly kill the browser from the harness
+        # because the test didn't return a result after reaching the test-internal timeout
+        status_subns = {"INTERNAL-ERROR": "ERROR",
+                        "EXTERNAL-TIMEOUT": "TIMEOUT"}
         expected = test.expected()
-        status = file_result.status if file_result.status != "EXTERNAL-TIMEOUT" else "TIMEOUT"
+        status = status_subns.get(file_result.status, file_result.status)
 
-        if file_result.status in ("TIMEOUT", "EXTERNAL-TIMEOUT"):
+        if file_result.status in ("TIMEOUT", "EXTERNAL-TIMEOUT", "INTERNAL-ERROR"):
             if self.browser.check_for_crashes():
                 status = "CRASH"
 
+        self.test_count += 1
         is_unexpected = expected != status
         if is_unexpected:
             self.unexpected_count += 1
@@ -583,7 +590,7 @@
                              extra=file_result.extra)
 
         restart_before_next = (test.restart_after or
-                               file_result.status in ("CRASH", "EXTERNAL-TIMEOUT") or
+                               file_result.status in ("CRASH", "EXTERNAL-TIMEOUT", "INTERNAL-ERROR") or
                                ((subtest_unexpected or is_unexpected) and
                                 self.restart_on_unexpected))
 
@@ -789,5 +796,8 @@
         self.stop_flag.set()
         self.logger.debug("Stop flag set in ManagerGroup")
 
+    def test_count(self):
+        return sum(item.test_count for item in self.pool)
+
     def unexpected_count(self):
         return sum(item.unexpected_count for item in self.pool)
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/tests/base.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/tests/base.py
new file mode 100644
index 0000000..b5173f3
--- /dev/null
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/tests/base.py
@@ -0,0 +1,60 @@
+import os
+import sys
+
+from os.path import dirname, join
+
+import pytest
+
+sys.path.insert(0, join(dirname(__file__), "..", ".."))
+
+from wptrunner import browsers
+
+
+_products = browsers.product_list
+_active_products = set()
+
+if "CURRENT_TOX_ENV" in os.environ:
+    current_tox_env_split = os.environ["CURRENT_TOX_ENV"].split("-")
+
+    tox_env_extra_browsers = {
+        "chrome": {"chrome_android"},
+        "servo": {"servodriver"},
+    }
+
+    _active_products = set(_products) & set(current_tox_env_split)
+    for product in frozenset(_active_products):
+        _active_products |= tox_env_extra_browsers.get(product, set())
+else:
+    _active_products = set(_products)
+
+
+class all_products(object):
+    def __init__(self, arg, marks={}):
+        self.arg = arg
+        self.marks = marks
+
+    def __call__(self, f):
+        params = []
+        for product in _products:
+            if product in self.marks:
+                params.append(pytest.param(product, marks=self.marks[product]))
+            else:
+                params.append(product)
+        return pytest.mark.parametrize(self.arg, params)(f)
+
+
+class active_products(object):
+    def __init__(self, arg, marks={}):
+        self.arg = arg
+        self.marks = marks
+
+    def __call__(self, f):
+        params = []
+        for product in _products:
+            if product not in _active_products:
+                params.append(pytest.param(product, marks=pytest.mark.skip(reason="wrong toxenv")))
+            elif product in self.marks:
+                params.append(pytest.param(product, marks=self.marks[product]))
+            else:
+                params.append(product)
+        return pytest.mark.parametrize(self.arg, params)(f)
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/tests/test_products.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/tests/test_products.py
new file mode 100644
index 0000000..c7109c5
--- /dev/null
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/tests/test_products.py
@@ -0,0 +1,65 @@
+import os
+import sys
+
+from os.path import join, dirname
+
+import mock
+import pytest
+
+from .base import all_products, active_products
+
+sys.path.insert(0, join(dirname(__file__), "..", "..", "..", ".."))  # repo root
+
+from tools import localpaths
+
+import sslutils
+
+from wptrunner import environment
+from wptrunner import products
+
+test_paths = {"/": {"tests_path": join(dirname(__file__), "..", "..", "..", "..")}}  # repo root
+environment.do_delayed_imports(None, test_paths)
+
+
+@active_products("product")
+def test_load_active_product(product):
+    """test we can successfully load the product of the current testenv"""
+    products.load_product({}, product)
+    # test passes if it doesn't throw
+
+
+@all_products("product")
+def test_load_all_products(product):
+    """test every product either loads or throws ImportError"""
+    try:
+        products.load_product({}, product)
+    except ImportError:
+        pass
+
+
+@active_products("product", marks={
+    "sauce": pytest.mark.skip("needs env extras kwargs"),
+})
+def test_server_start_config(product):
+    (check_args,
+     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,
+                                         sslutils.environments["none"](None),
+                                         False,
+                                         None,
+                                         env_options,
+                                         env_extras) as test_environment:
+            start.assert_called_once()
+            args = start.call_args
+            config = args[0][0]
+            if "server_host" in env_options:
+                assert config["server_host"] == env_options["server_host"]
+            else:
+                assert config["server_host"] == config["browser_host"]
+            assert isinstance(config["bind_address"], bool)
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/update/metadata.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/update/metadata.py
index 67f46de..c70ec61 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/update/metadata.py
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/update/metadata.py
@@ -4,14 +4,14 @@
 
 from base import Step, StepRunner
 
+
 class GetUpdatePropertyList(Step):
     provides = ["property_order", "boolean_properties"]
 
-
     def create(self, state):
         property_order, boolean_properties = products.load_product_update(
             state.config, state.product)
-        state.property_order = property_order
+        state.property_order = property_order + state.extra_properties
         state.boolean_properties = boolean_properties
 
 
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/update/update.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/update/update.py
index c985d35..91029c5 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/update/update.py
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/update/update.py
@@ -92,6 +92,7 @@
             state.suite_name = kwargs["suite_name"]
             state.product = kwargs["product"]
             state.config = kwargs["config"]
+            state.extra_properties = kwargs["extra_property"]
             runner = MetadataUpdateRunner(self.logger, state)
             runner.run()
 
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/webdriver_server.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/webdriver_server.py
index 8a3f9c9..5822b36 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/webdriver_server.py
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/webdriver_server.py
@@ -13,7 +13,7 @@
 
 __all__ = ["SeleniumServer", "ChromeDriverServer", "OperaDriverServer",
            "GeckoDriverServer", "InternetExplorerDriverServer", "EdgeDriverServer",
-           "ServoDriverServer", "WebDriverServer"]
+           "ServoDriverServer", "WebKitDriverServer", "WebDriverServer"]
 
 
 class WebDriverServer(object):
@@ -60,21 +60,22 @@
             env=self.env,
             storeOutput=False)
 
+        self.logger.debug("Starting WebDriver: %s" % ' '.join(self._cmd))
         try:
             self._proc.run()
         except OSError as e:
             if e.errno == errno.ENOENT:
                 raise IOError(
-                    "WebDriver HTTP server executable not found: %s" % self.binary)
+                    "WebDriver executable not found: %s" % self.binary)
             raise
 
         self.logger.debug(
-            "Waiting for server to become accessible: %s" % self.url)
+            "Waiting for WebDriver to become accessible: %s" % self.url)
         try:
             wait_for_service((self.host, self.port))
         except Exception:
             self.logger.error(
-                "WebDriver HTTP server was not accessible "
+                "WebDriver was not accessible "
                 "within the timeout:\n%s" % traceback.format_exc())
             raise
 
@@ -178,6 +179,16 @@
                 "--port", str(self.port)] + self._args
 
 
+class SafariDriverServer(WebDriverServer):
+    def __init__(self, logger, binary="safaridriver", port=None, args=None):
+        WebDriverServer.__init__(
+            self, logger, binary, port=port, args=args)
+
+    def make_command(self):
+        return [self.binary,
+                "--port=%s" % str(self.port)] + self._args
+
+
 class ServoDriverServer(WebDriverServer):
     def __init__(self, logger, binary="servo", binary_args=None, host="127.0.0.1",
                  port=None, args=None):
@@ -196,6 +207,14 @@
         return command
 
 
+class WebKitDriverServer(WebDriverServer):
+    def __init__(self, logger, binary=None, port=None, args=None):
+        WebDriverServer.__init__(self, logger, binary, port=port, args=args)
+
+    def make_command(self):
+        return [self.binary, "--port=%s" % str(self.port)] + self._args
+
+
 def cmd_arg(name, value=None):
     prefix = "-" if platform.system() == "Windows" else "--"
     rv = prefix + name
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/wptcommandline.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/wptcommandline.py
index ad18e42..8361ca1 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/wptcommandline.py
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/wptcommandline.py
@@ -4,6 +4,7 @@
 import sys
 from collections import OrderedDict
 from distutils.spawn import find_executable
+from datetime import datetime, timedelta
 
 import config
 import wpttest
@@ -82,6 +83,36 @@
     mode_group.add_argument("--verify-log-full", action="store_true",
                             default=False,
                             help="Output per-iteration test results when running verify")
+    mode_group.add_argument("--verify-repeat-loop", action="store",
+                            default=10,
+                            help="Number of iterations for a run that reloads each test without restart.",
+                            type=int)
+    mode_group.add_argument("--verify-repeat-restart", action="store",
+                            default=5,
+                            help="Number of iterations, for a run that restarts the runner between each iteration",
+                            type=int)
+    chaos_mode_group = mode_group.add_mutually_exclusive_group()
+    chaos_mode_group.add_argument("--verify-no-chaos-mode", action="store_false",
+                                  default=True,
+                                  dest="verify_chaos_mode",
+                                  help="Disable chaos mode when running on Firefox")
+    chaos_mode_group.add_argument("--verify-chaos-mode", action="store_true",
+                                  default=True,
+                                  dest="verify_chaos_mode",
+                                  help="Enable chaos mode when running on Firefox")
+    mode_group.add_argument("--verify-max-time", action="store",
+                            default=None,
+                            help="The maximum number of minutes for the job to run",
+                            type=lambda x: timedelta(minutes=float(x)))
+    output_results_group = mode_group.add_mutually_exclusive_group()
+    output_results_group.add_argument("--verify-no-output-results", action="store_false",
+                                      dest="verify_output_results",
+                                      default=True,
+                                      help="Prints individuals test results and messages")
+    output_results_group.add_argument("--verify-output-results", action="store_true",
+                                      dest="verify_output_results",
+                                      default=True,
+                                      help="Disable printing individuals test results and messages")
 
     test_selection_group = parser.add_argument_group("Test Selection")
     test_selection_group.add_argument("--test-types", action="store",
@@ -145,6 +176,8 @@
                               help="Path to root directory containing test metadata"),
     config_group.add_argument("--tests", action="store", type=abs_path, dest="tests_root",
                               help="Path to root directory containing test files"),
+    config_group.add_argument("--manifest", action="store", type=abs_path, dest="manifest_path",
+                              help="Path to test manifest (default is ${metadata_root}/MANIFEST.json)")
     config_group.add_argument("--run-info", action="store", type=abs_path,
                               help="Path to directory containing extra json files to add to run info")
     config_group.add_argument("--product", action="store", choices=product_choices,
@@ -245,6 +278,10 @@
                              dest="sauce_connect_binary",
                              help="Path to Sauce Connect binary")
 
+    webkit_group = parser.add_argument_group("WebKit-specific")
+    webkit_group.add_argument("--webkit-port", dest="webkit_port",
+                             help="WebKit port")
+
     parser.add_argument("test_list", nargs="*",
                         help="List of URLs for tests to run, or paths including tests to run. "
                              "(equivalent to --include)")
@@ -297,9 +334,16 @@
             kwargs["test_paths"]["/"] = {}
         kwargs["test_paths"]["/"]["metadata_path"] = kwargs["metadata_root"]
 
+    if kwargs.get("manifest_path"):
+        if "/" not in kwargs["test_paths"]:
+            kwargs["test_paths"]["/"] = {}
+        kwargs["test_paths"]["/"]["manifest_path"] = kwargs["manifest_path"]
+
     kwargs["suite_name"] = kwargs["config"].get("web-platform-tests", {}).get("name", "web-platform-tests")
 
 
+    check_paths(kwargs)
+
 def get_test_paths(config):
     # Set up test_paths
     test_paths = OrderedDict()
@@ -310,7 +354,10 @@
             url_base = manifest_opts.get("url_base", "/")
             test_paths[url_base] = {
                 "tests_path": manifest_opts.get_path("tests"),
-                "metadata_path": manifest_opts.get_path("metadata")}
+                "metadata_path": manifest_opts.get_path("metadata"),
+            }
+            if "manifest" in manifest_opts:
+                test_paths[url_base]["manifest_path"] = manifest_opts.get_path("manifest")
 
     return test_paths
 
@@ -326,17 +373,23 @@
         return None
 
 
-def check_args(kwargs):
-    set_from_config(kwargs)
-
+def check_paths(kwargs):
     for test_paths in kwargs["test_paths"].itervalues():
         if not ("tests_path" in test_paths and
                 "metadata_path" in test_paths):
             print "Fatal: must specify both a test path and metadata path"
             sys.exit(1)
+        if "manifest_path" not in test_paths:
+            test_paths["manifest_path"] = os.path.join(test_paths["metadata_path"],
+                                                       "MANIFEST.json")
         for key, path in test_paths.iteritems():
             name = key.split("_", 1)[0]
 
+            if name == "manifest":
+                # For the manifest we can create it later, so just check the path
+                # actually exists
+                path = os.path.dirname(path)
+
             if not os.path.exists(path):
                 print "Fatal: %s path %s does not exist" % (name, path)
                 sys.exit(1)
@@ -345,6 +398,10 @@
                 print "Fatal: %s path %s is not a directory" % (name, path)
                 sys.exit(1)
 
+
+def check_args(kwargs):
+    set_from_config(kwargs)
+
     if kwargs["product"] is None:
         kwargs["product"] = "firefox"
 
@@ -467,6 +524,8 @@
                         help="Path to the folder containing test metadata"),
     parser.add_argument("--tests", action="store", type=abs_path, dest="tests_root",
                         help="Path to web-platform-tests"),
+    parser.add_argument("--manifest", action="store", type=abs_path, dest="manifest_path",
+                        help="Path to test manifest (default is ${metadata_root}/MANIFEST.json)")
     parser.add_argument("--sync-path", action="store", type=abs_path,
                         help="Path to store git checkout of web-platform-tests during update"),
     parser.add_argument("--remote_url", action="store",
@@ -491,6 +550,8 @@
                         help="List of glob-style paths to exclude when syncing tests")
     parser.add_argument("--include", action="store", nargs="*",
                         help="List of glob-style paths to include which would otherwise be excluded when syncing tests")
+    parser.add_argument("--extra-property", action="append", default=[],
+                        help="Extra property from run_info.json to use in metadata update")
     # Should make this required iff run=logfile
     parser.add_argument("run_log", nargs="*", type=abs_path,
                         help="Log file from run of tests")
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/wptrunner.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/wptrunner.py
index d3d9d07..40863d8 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/wptrunner.py
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/wptrunner.py
@@ -173,6 +173,7 @@
 
         logger.info("Using %i client processes" % kwargs["processes"])
 
+        test_total = 0
         unexpected_total = 0
 
         kwargs["pause_after_test"] = get_pause_after_test(test_loader, **kwargs)
@@ -200,6 +201,7 @@
                 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)
                 for test_type in kwargs["test_types"]:
@@ -267,20 +269,32 @@
                             logger.critical("Main thread got signal")
                             manager_group.stop()
                             raise
+                    test_count += manager_group.test_count()
                     unexpected_count += manager_group.unexpected_count()
 
+                test_total += test_count
                 unexpected_total += unexpected_count
                 logger.info("Got %i unexpected results" % unexpected_count)
                 if repeat_until_unexpected and unexpected_total > 0:
                     break
                 logger.suite_end()
+
+    if test_total == 0:
+        logger.error("No tests ran")
+        return False
+
     return unexpected_total == 0
 
 
 def check_stability(**kwargs):
     import stability
-    return stability.check_stability(logger, **kwargs)
-
+    return stability.check_stability(logger,
+                                     max_time=kwargs['verify_max_time'],
+                                     chaos_mode=kwargs['verify_chaos_mode'],
+                                     repeat_loop=kwargs['verify_repeat_loop'],
+                                     repeat_restart=kwargs['verify_repeat_restart'],
+                                     output_results=kwargs['verify_output_results'],
+                                     **kwargs)
 
 def start(**kwargs):
     if kwargs["list_test_groups"]:
diff --git a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/wpttest.py b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/wpttest.py
index 9f4c1be..0fb1bdd 100644
--- a/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/wpttest.py
+++ b/WebDriverTests/imported/w3c/tools/wptrunner/wptrunner/wpttest.py
@@ -36,7 +36,7 @@
 
 class TestharnessResult(Result):
     default_expected = "OK"
-    statuses = set(["OK", "ERROR", "TIMEOUT", "EXTERNAL-TIMEOUT", "CRASH"])
+    statuses = set(["OK", "ERROR", "INTERNAL-ERROR", "TIMEOUT", "EXTERNAL-TIMEOUT", "CRASH"])
 
 
 class TestharnessSubtestResult(SubtestResult):
@@ -46,12 +46,13 @@
 
 class ReftestResult(Result):
     default_expected = "PASS"
-    statuses = set(["PASS", "FAIL", "ERROR", "TIMEOUT", "EXTERNAL-TIMEOUT", "CRASH"])
+    statuses = set(["PASS", "FAIL", "ERROR", "INTERNAL-ERROR", "TIMEOUT", "EXTERNAL-TIMEOUT",
+                    "CRASH"])
 
 
 class WdspecResult(Result):
     default_expected = "OK"
-    statuses = set(["OK", "ERROR", "TIMEOUT", "EXTERNAL-TIMEOUT", "CRASH"])
+    statuses = set(["OK", "ERROR", "INTERNAL-ERROR", "TIMEOUT", "EXTERNAL-TIMEOUT", "CRASH"])
 
 
 class WdspecSubtestResult(SubtestResult):
diff --git a/WebDriverTests/imported/w3c/webdriver/tests/actions/modifier_click.py b/WebDriverTests/imported/w3c/webdriver/tests/actions/modifier_click.py
index fdd43e2..85a23d4 100644
--- a/WebDriverTests/imported/w3c/webdriver/tests/actions/modifier_click.py
+++ b/WebDriverTests/imported/w3c/webdriver/tests/actions/modifier_click.py
@@ -1,39 +1,9 @@
-# META: timeout=long
-
 import pytest
 
 from tests.actions.support.refine import filter_dict, get_events
 from tests.actions.support.keys import Keys
 
 
-# Using local fixtures because we want to start a new session between
-# each test, otherwise the clicks in each test interfere with each other.
-@pytest.fixture(autouse=True)
-def release_actions(mod_click_session, request):
-    request.addfinalizer(mod_click_session.actions.release)
-
-
-@pytest.fixture
-def mod_click_session(new_session, url, add_browser_capabilites):
-    _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({})}})
-    session.url = url("/webdriver/tests/actions/support/test_actions_wdspec.html")
-
-    return session
-
-
-@pytest.fixture
-def key_chain(mod_click_session):
-    return mod_click_session.actions.sequence("key", "keyboard_id")
-
-
-@pytest.fixture
-def mouse_chain(mod_click_session):
-    return mod_click_session.actions.sequence(
-        "pointer",
-        "pointer_id",
-        {"pointerType": "mouse"})
-
-
 @pytest.mark.parametrize("modifier, prop", [
    (Keys.ALT, "altKey"),
    (Keys.R_ALT, "altKey"),
@@ -42,19 +12,15 @@
    (Keys.SHIFT, "shiftKey"),
    (Keys.R_SHIFT, "shiftKey"),
 ])
-def test_modifier_click(mod_click_session,
-                       key_chain,
-                       mouse_chain,
-                       modifier,
-                       prop):
+def test_modifier_click(session, test_actions_page, key_chain, mouse_chain, modifier, prop):
     key_chain \
         .pause(200) \
         .key_down(modifier) \
         .pause(200) \
         .key_up(modifier)
-    outer = mod_click_session.find.css("#outer", all=False)
+    outer = session.find.css("#outer", all=False)
     mouse_chain.click(element=outer)
-    mod_click_session.actions.perform([key_chain.dict, mouse_chain.dict])
+    session.actions.perform([key_chain.dict, mouse_chain.dict])
     expected = [
         {"type": "mousemove"},
         {"type": "mousedown"},
@@ -71,12 +37,12 @@
         e.update(defaults)
         if e["type"] != "mousemove":
             e[prop] = True
-    filtered_events = [filter_dict(e, expected[0]) for e in get_events(mod_click_session)]
+    filtered_events = [filter_dict(e, expected[0]) for e in get_events(session)]
     assert expected == filtered_events
 
 
-def test_many_modifiers_click(mod_click_session, key_chain, mouse_chain):
-    outer = mod_click_session.find.css("#outer", all=False)
+def test_many_modifiers_click(session, test_actions_page, key_chain, mouse_chain):
+    outer = session.find.css("#outer", all=False)
     key_chain \
         .pause(0) \
         .key_down(Keys.CONTROL) \
@@ -92,7 +58,7 @@
         .pause(0) \
         .pause(0) \
         .pointer_down()
-    mod_click_session.actions.perform([key_chain.dict, mouse_chain.dict])
+    session.actions.perform([key_chain.dict, mouse_chain.dict])
     expected = [
         {"type": "mousemove"},
         # shift and ctrl presses
@@ -113,5 +79,5 @@
     for e in expected[1:4]:
         e["shiftKey"] = True
         e["ctrlKey"] = True
-    events = [filter_dict(e, expected[0]) for e in get_events(mod_click_session)]
+    events = [filter_dict(e, expected[0]) for e in get_events(session)]
     assert events == expected
diff --git a/WebDriverTests/imported/w3c/webdriver/tests/actions/mouse_dblclick.py b/WebDriverTests/imported/w3c/webdriver/tests/actions/mouse_dblclick.py
index bdd3905..fc53a51 100644
--- a/WebDriverTests/imported/w3c/webdriver/tests/actions/mouse_dblclick.py
+++ b/WebDriverTests/imported/w3c/webdriver/tests/actions/mouse_dblclick.py
@@ -1,40 +1,11 @@
 import pytest
 
-from tests.actions.support.mouse import get_inview_center, get_viewport_rect
 from tests.actions.support.refine import get_events, filter_dict
 from tests.support.asserts import assert_move_to_coordinates
 
 
-_DBLCLICK_INTERVAL = 640
-
-
-# Using local fixtures because we want to start a new session between
-# each test, otherwise the clicks in each test interfere with each other.
-@pytest.fixture(autouse=True)
-def release_actions(dblclick_session, request):
-    # release all actions after each test
-    # equivalent to a teardown_function, but with access to session fixture
-    request.addfinalizer(dblclick_session.actions.release)
-
-
-@pytest.fixture
-def dblclick_session(new_session, url, add_browser_capabilites):
-    _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({})}})
-    session.url = url("/webdriver/tests/actions/support/test_actions_wdspec.html")
-
-    return session
-
-
-@pytest.fixture
-def mouse_chain(dblclick_session):
-    return dblclick_session.actions.sequence(
-        "pointer",
-        "pointer_id",
-        {"pointerType": "mouse"})
-
-
 @pytest.mark.parametrize("click_pause", [0, 200])
-def test_dblclick_at_coordinates(dblclick_session, mouse_chain, click_pause):
+def test_dblclick_at_coordinates(session, test_actions_page, mouse_chain, click_pause):
     div_point = {
         "x": 82,
         "y": 187,
@@ -45,7 +16,7 @@
         .pause(click_pause) \
         .click() \
         .perform()
-    events = get_events(dblclick_session)
+    events = get_events(session)
     assert_move_to_coordinates(div_point, "outer", events)
     expected = [
         {"type": "mousedown", "button": 0},
@@ -59,51 +30,3 @@
     assert len(events) == 8
     filtered_events = [filter_dict(e, expected[0]) for e in events]
     assert expected == filtered_events[1:]
-
-
-def test_dblclick_with_pause_after_second_pointerdown(dblclick_session, mouse_chain):
-        outer = dblclick_session.find.css("#outer", all=False)
-        center = get_inview_center(outer.rect, get_viewport_rect(dblclick_session))
-        mouse_chain \
-            .pointer_move(int(center["x"]), int(center["y"])) \
-            .click() \
-            .pointer_down() \
-            .pause(_DBLCLICK_INTERVAL + 10) \
-            .pointer_up() \
-            .perform()
-        events = get_events(dblclick_session)
-        expected = [
-            {"type": "mousedown", "button": 0},
-            {"type": "mouseup", "button": 0},
-            {"type": "click", "button": 0},
-            {"type": "mousedown", "button": 0},
-            {"type": "mouseup", "button": 0},
-            {"type": "click", "button": 0},
-            {"type": "dblclick", "button": 0},
-        ]
-        assert len(events) == 8
-        filtered_events = [filter_dict(e, expected[0]) for e in events]
-        assert expected == filtered_events[1:]
-
-
-def test_no_dblclick(dblclick_session, mouse_chain):
-        outer = dblclick_session.find.css("#outer", all=False)
-        center = get_inview_center(outer.rect, get_viewport_rect(dblclick_session))
-        mouse_chain \
-            .pointer_move(int(center["x"]), int(center["y"])) \
-            .click() \
-            .pause(_DBLCLICK_INTERVAL + 10) \
-            .click() \
-            .perform()
-        events = get_events(dblclick_session)
-        expected = [
-            {"type": "mousedown", "button": 0},
-            {"type": "mouseup", "button": 0},
-            {"type": "click", "button": 0},
-            {"type": "mousedown", "button": 0},
-            {"type": "mouseup", "button": 0},
-            {"type": "click", "button": 0},
-        ]
-        assert len(events) == 7
-        filtered_events = [filter_dict(e, expected[0]) for e in events]
-        assert expected == filtered_events[1:]
diff --git a/WebDriverTests/imported/w3c/webdriver/tests/actions/mouse_pause_dblclick.py b/WebDriverTests/imported/w3c/webdriver/tests/actions/mouse_pause_dblclick.py
new file mode 100644
index 0000000..ad17967
--- /dev/null
+++ b/WebDriverTests/imported/w3c/webdriver/tests/actions/mouse_pause_dblclick.py
@@ -0,0 +1,52 @@
+from tests.actions.support.mouse import get_inview_center, get_viewport_rect
+from tests.actions.support.refine import get_events, filter_dict
+
+_DBLCLICK_INTERVAL = 640
+
+
+def test_dblclick_with_pause_after_second_pointerdown(session, test_actions_page, mouse_chain):
+        outer = session.find.css("#outer", all=False)
+        center = get_inview_center(outer.rect, get_viewport_rect(session))
+        mouse_chain \
+            .pointer_move(int(center["x"]), int(center["y"])) \
+            .click() \
+            .pointer_down() \
+            .pause(_DBLCLICK_INTERVAL + 10) \
+            .pointer_up() \
+            .perform()
+        events = get_events(session)
+        expected = [
+            {"type": "mousedown", "button": 0},
+            {"type": "mouseup", "button": 0},
+            {"type": "click", "button": 0},
+            {"type": "mousedown", "button": 0},
+            {"type": "mouseup", "button": 0},
+            {"type": "click", "button": 0},
+            {"type": "dblclick", "button": 0},
+        ]
+        assert len(events) == 8
+        filtered_events = [filter_dict(e, expected[0]) for e in events]
+        assert expected == filtered_events[1:]
+
+
+def test_no_dblclick(session, test_actions_page, mouse_chain):
+        outer = session.find.css("#outer", all=False)
+        center = get_inview_center(outer.rect, get_viewport_rect(session))
+        mouse_chain \
+            .pointer_move(int(center["x"]), int(center["y"])) \
+            .click() \
+            .pause(_DBLCLICK_INTERVAL + 10) \
+            .click() \
+            .perform()
+        events = get_events(session)
+        expected = [
+            {"type": "mousedown", "button": 0},
+            {"type": "mouseup", "button": 0},
+            {"type": "click", "button": 0},
+            {"type": "mousedown", "button": 0},
+            {"type": "mouseup", "button": 0},
+            {"type": "click", "button": 0},
+        ]
+        assert len(events) == 7
+        filtered_events = [filter_dict(e, expected[0]) for e in events]
+        assert expected == filtered_events[1:]
diff --git a/WebDriverTests/imported/w3c/webdriver/tests/actions/special_keys.py b/WebDriverTests/imported/w3c/webdriver/tests/actions/special_keys.py
index f50bbc6..d2a4422 100644
--- a/WebDriverTests/imported/w3c/webdriver/tests/actions/special_keys.py
+++ b/WebDriverTests/imported/w3c/webdriver/tests/actions/special_keys.py
@@ -2,9 +2,10 @@
 
 import pytest
 import time
+from tests.support.fixtures import configuration
 from tests.actions.support.keys import ALL_EVENTS, Keys
 from tests.actions.support.refine import filter_dict, get_keys, get_events
-
+from webdriver import error
 
 @pytest.mark.parametrize("name,expected", ALL_EVENTS.items())
 def test_webdriver_special_key_sends_keydown(session,
@@ -20,6 +21,9 @@
             document.body.addEventListener("keydown",
                     function(e) { e.preventDefault() });
         """)
+    if (session.capabilities["browserName"] == 'internet explorer'):
+        key_reporter.click()
+        session.execute_script("resetEvents();")
     key_chain.key_down(getattr(Keys, name)).perform()
 
     # only interested in keydown
@@ -43,3 +47,37 @@
         assert entered_keys == expected["key"]
     else:
         assert len(entered_keys) == 0
+
+
+@pytest.mark.parametrize("value", [
+    (u"f"),
+    (u"\u0BA8\u0BBF"),
+    (u"\u1100\u1161\u11A8"),
+])
+def test_multiple_codepoint_keys_behave_correctly(session,
+                                                  key_reporter,
+                                                  key_chain,
+                                                  value):
+    key_chain \
+        .key_down(value) \
+        .key_up(value) \
+        .perform()
+
+    assert get_keys(key_reporter) == value
+
+
+@pytest.mark.parametrize("value", [
+    (u"fa"),
+    (u"\u0BA8\u0BBFb"),
+    (u"\u0BA8\u0BBF\u0BA8"),
+    (u"\u1100\u1161\u11A8c")
+])
+def test_invalid_multiple_codepoint_keys_fail(session,
+                                              key_reporter,
+                                              key_chain,
+                                              value):
+    with pytest.raises(error.InvalidArgumentException):
+        key_chain \
+            .key_down(value) \
+            .key_up(value) \
+            .perform()
\ No newline at end of file
diff --git a/WebDriverTests/imported/w3c/webdriver/tests/contexts/maximize_window.py b/WebDriverTests/imported/w3c/webdriver/tests/contexts/maximize_window.py
index e9257e6..6143dc1 100644
--- a/WebDriverTests/imported/w3c/webdriver/tests/contexts/maximize_window.py
+++ b/WebDriverTests/imported/w3c/webdriver/tests/contexts/maximize_window.py
@@ -12,6 +12,12 @@
     return session.transport.send("POST", "session/%s/window/maximize" % session.session_id)
 
 
+def is_fullscreen(session):
+    # At the time of writing, WebKit does not conform to the Fullscreen API specification.
+    # Remove the prefixed fallback when https://bugs.webkit.org/show_bug.cgi?id=158125 is fixed.
+    return session.execute_script("return !!(window.fullScreen || document.webkitIsFullScreen)")
+
+
 # 10.7.3 Maximize Window
 
 
@@ -137,11 +143,11 @@
 
     """
     session.window.fullscreen()
-    assert session.execute_script("return window.fullScreen") is True
+    assert is_fullscreen(session) is True
 
     response = maximize(session)
     assert_success(response)
-    assert session.execute_script("return window.fullScreen") is False
+    assert is_fullscreen(session) is False
 
 
 def test_restore_the_window(session):
diff --git a/WebDriverTests/imported/w3c/webdriver/tests/cookies/add_cookie.py b/WebDriverTests/imported/w3c/webdriver/tests/cookies/add_cookie.py
index ff4a0c9..88b79fb 100644
--- a/WebDriverTests/imported/w3c/webdriver/tests/cookies/add_cookie.py
+++ b/WebDriverTests/imported/w3c/webdriver/tests/cookies/add_cookie.py
@@ -36,21 +36,22 @@
 
     assert cookie["name"] == "hello"
     assert cookie["value"] == "world"
-    assert cookie["domain"] == ".%s" % server_config["domains"][""]
+    assert cookie["domain"] == ".%s" % server_config["domains"][""] or cookie["domain"] == "%s" % server_config["domains"][""]
 
 def test_add_cookie_for_ip(session, url, server_config, configuration):
-    session.url = "http://127.0.0.1:%s/404" % (server_config["ports"]["http"][0])
+    session.url = "http://127.0.0.1:%s/common/blank.html" % (server_config["ports"]["http"][0])
     clear_all_cookies(session)
     create_cookie_request = {
         "cookie": {
             "name": "hello",
             "value": "world",
-            "domain": configuration["host"],
+            "domain": "127.0.0.1",
             "path": "/",
             "httpOnly": False,
             "secure": False
         }
     }
+
     result = session.transport.send("POST", "session/%s/cookie" % session.session_id, create_cookie_request)
     assert result.status == 200
     assert "value" in result.body
@@ -78,7 +79,7 @@
 def test_add_non_session_cookie(session, url):
     session.url = url("/common/blank.html")
     clear_all_cookies(session)
-    a_year_from_now = int((datetime.utcnow() + timedelta(days=365)).strftime("%s"))
+    a_year_from_now = int((datetime.utcnow() + timedelta(days=365) - datetime.utcfromtimestamp(0)).total_seconds())
     create_cookie_request = {
         "cookie": {
             "name": "hello",
@@ -174,4 +175,4 @@
 
     assert cookie["name"] == "hello"
     assert cookie["value"] == "world"
-    assert cookie["domain"] == ".%s" % server_config["domains"][""]
+    assert cookie["domain"] == ".%s" % server_config["domains"][""] or cookie["domain"] == "%s" % server_config["domains"][""]
diff --git a/WebDriverTests/imported/w3c/webdriver/tests/element_retrieval/find_element.py b/WebDriverTests/imported/w3c/webdriver/tests/element_retrieval/find_element.py
index 454e3c3..dddb28cb 100644
--- a/WebDriverTests/imported/w3c/webdriver/tests/element_retrieval/find_element.py
+++ b/WebDriverTests/imported/w3c/webdriver/tests/element_retrieval/find_element.py
@@ -51,6 +51,39 @@
     assert_success(response)
 
 
+@pytest.mark.parametrize("document,value", [
+    ("<a href=#>link text</a>", "link text"),
+    ("<a href=#>&nbsp;link text&nbsp;</a>", "link text"),
+    ("<a href=#>link<br>text</a>", "link\ntext"),
+    ("<a href=#>link&amp;text</a>", "link&text"),
+    ("<a href=#>LINK TEXT</a>", "LINK TEXT"),
+    ("<a href=# style='text-transform: uppercase'>link text</a>", "LINK TEXT"),
+])
+def test_find_element_link_text(session, document, value):
+    # Step 8 - 9
+    session.url = inline(document)
+
+    response = find_element(session, "link text", value)
+    assert_success(response)
+
+
+@pytest.mark.parametrize("document,value", [
+    ("<a href=#>partial link text</a>", "link"),
+    ("<a href=#>&nbsp;partial link text&nbsp;</a>", "link"),
+    ("<a href=#>partial link text</a>", "k t"),
+    ("<a href=#>partial link<br>text</a>", "k\nt"),
+    ("<a href=#>partial link&amp;text</a>", "k&t"),
+    ("<a href=#>PARTIAL LINK TEXT</a>", "LINK"),
+    ("<a href=# style='text-transform: uppercase'>partial link text</a>", "LINK"),
+])
+def test_find_element_partial_link_text(session, document, value):
+    # Step 8 - 9
+    session.url = inline(document)
+
+    response = find_element(session, "partial link text", value)
+    assert_success(response)
+
+
 @pytest.mark.parametrize("using,value", [("css selector", "#wontExist")])
 def test_no_element(session, using, value):
     # Step 8 - 9
@@ -65,7 +98,8 @@
                           ("tag name", "a"),
                           ("xpath", "//*[name()='a']")])
 def test_xhtml_namespace(session, using, value):
-    session.url = inline("""<a href="#" id="linkText">full link text</a>""", doctype="xhtml")
+    session.url = inline("""<a href="#" id="linkText">full link text</a>""",
+                         doctype="xhtml")
     expected = session.execute_script("return document.links[0]")
 
     response = find_element(session, using, value)
diff --git a/WebDriverTests/imported/w3c/webdriver/tests/element_retrieval/find_element_from_element.py b/WebDriverTests/imported/w3c/webdriver/tests/element_retrieval/find_element_from_element.py
index d79d512..f6b9858 100644
--- a/WebDriverTests/imported/w3c/webdriver/tests/element_retrieval/find_element_from_element.py
+++ b/WebDriverTests/imported/w3c/webdriver/tests/element_retrieval/find_element_from_element.py
@@ -31,7 +31,6 @@
     session.close()
 
     response = find_element(session, "notReal", "css selector", "foo")
-
     assert_error(response, "no such window")
 
 
@@ -49,6 +48,41 @@
     assert_success(response)
 
 
+@pytest.mark.parametrize("document,value", [
+    ("<a href=#>link text</a>", "link text"),
+    ("<a href=#>&nbsp;link text&nbsp;</a>", "link text"),
+    ("<a href=#>link<br>text</a>", "link\ntext"),
+    ("<a href=#>link&amp;text</a>", "link&text"),
+    ("<a href=#>LINK TEXT</a>", "LINK TEXT"),
+    ("<a href=# style='text-transform: uppercase'>link text</a>", "LINK TEXT"),
+])
+def test_find_element_link_text(session, document, value):
+    # Step 8 - 9
+    session.url = inline("<div>{0}</div>".format(document))
+    element = session.find.css("div", all=False)
+
+    response = find_element(session, element.id, "link text", value)
+    assert_success(response)
+
+
+@pytest.mark.parametrize("document,value", [
+    ("<a href=#>partial link text</a>", "link"),
+    ("<a href=#>&nbsp;partial link text&nbsp;</a>", "link"),
+    ("<a href=#>partial link text</a>", "k t"),
+    ("<a href=#>partial link<br>text</a>", "k\nt"),
+    ("<a href=#>partial link&amp;text</a>", "k&t"),
+    ("<a href=#>PARTIAL LINK TEXT</a>", "LINK"),
+    ("<a href=# style='text-transform: uppercase'>partial link text</a>", "LINK"),
+])
+def test_find_element_partial_link_text(session, document, value):
+    # Step 8 - 9
+    session.url = inline("<div>{0}</div>".format(document))
+    element = session.find.css("div", all=False)
+
+    response = find_element(session, element.id, "partial link text", value)
+    assert_success(response)
+
+
 @pytest.mark.parametrize("using,value",[("css selector", "#wontExist")])
 def test_no_element(session, using, value):
     # Step 8 - 9
@@ -65,7 +99,8 @@
                           ("tag name", "a"),
                           ("xpath", "//*[name()='a']")])
 def test_xhtml_namespace(session, using, value):
-    session.url = inline("""<p><a href="#" id="linkText">full link text</a></p>""", doctype="xhtml")
+    session.url = inline("""<p><a href="#" id="linkText">full link text</a></p>""",
+                         doctype="xhtml")
     from_element = session.execute_script("""return document.querySelector("p")""")
     expected = session.execute_script("return document.links[0]")
 
@@ -76,7 +111,17 @@
 
 def test_parent_htmldocument(session):
     session.url = inline("")
+    from_element = session.execute_script("""return document.querySelector("body")""")
+    expected = session.execute_script("return document.documentElement")
+
+    response = find_element(session, from_element.id, "xpath", "..")
+    value = assert_success(response)
+    assert_same_element(session, value, expected)
+
+
+def test_parent_of_document_node_errors(session):
+    session.url = inline("")
     from_element = session.execute_script("return document.documentElement")
 
     response = find_element(session, from_element.id, "xpath", "..")
-    assert_success(response)
+    assert_error(response, "invalid selector")
diff --git a/WebDriverTests/imported/w3c/webdriver/tests/element_retrieval/find_elements.py b/WebDriverTests/imported/w3c/webdriver/tests/element_retrieval/find_elements.py
index a00ae23..781dd3b 100644
--- a/WebDriverTests/imported/w3c/webdriver/tests/element_retrieval/find_elements.py
+++ b/WebDriverTests/imported/w3c/webdriver/tests/element_retrieval/find_elements.py
@@ -30,10 +30,7 @@
     session.window_handle = new_window
     session.close()
 
-    response = session.transport.send("POST",
-                                      "session/%s/elements" % session.session_id,
-                                      {"using": "css selector", "value": "foo"})
-
+    response = find_elements(session, "css selector", "foo")
     assert_error(response, "no such window")
 
 
@@ -52,6 +49,51 @@
     assert len(response.body["value"]) == 1
 
 
+@pytest.mark.parametrize("document,value", [
+    ("<a href=#>link text</a>", "link text"),
+    ("<a href=#>&nbsp;link text&nbsp;</a>", "link text"),
+    ("<a href=#>link<br>text</a>", "link\ntext"),
+    ("<a href=#>link&amp;text</a>", "link&text"),
+    ("<a href=#>LINK TEXT</a>", "LINK TEXT"),
+    ("<a href=# style='text-transform: uppercase'>link text</a>", "LINK TEXT"),
+])
+def test_find_elements_link_text(session, document, value):
+    # Step 8 - 9
+    session.url = inline("<a href=#>not wanted</a><br/>{0}".format(document))
+    expected = session.execute_script("return document.links[1];")
+
+    response = find_elements(session, "link text", value)
+    value = assert_success(response)
+    assert isinstance(value, list)
+    assert len(value) == 1
+
+    found_element = value[0]
+    assert_same_element(session, found_element, expected)
+
+
+@pytest.mark.parametrize("document,value", [
+    ("<a href=#>partial link text</a>", "link"),
+    ("<a href=#>&nbsp;partial link text&nbsp;</a>", "link"),
+    ("<a href=#>partial link text</a>", "k t"),
+    ("<a href=#>partial link<br>text</a>", "k\nt"),
+    ("<a href=#>partial link&amp;text</a>", "k&t"),
+    ("<a href=#>PARTIAL LINK TEXT</a>", "LINK"),
+    ("<a href=# style='text-transform: uppercase'>partial link text</a>", "LINK"),
+])
+def test_find_elements_partial_link_text(session, document, value):
+    # Step 8 - 9
+    session.url = inline("<a href=#>not wanted</a><br/>{0}".format(document))
+    expected = session.execute_script("return document.links[1];")
+
+    response = find_elements(session, "partial link text", value)
+    value = assert_success(response)
+    assert isinstance(value, list)
+    assert len(value) == 1
+
+    found_element = value[0]
+    assert_same_element(session, found_element, expected)
+
+
 @pytest.mark.parametrize("using,value", [("css selector", "#wontExist")])
 def test_no_element(session, using, value):
     # Step 8 - 9
@@ -67,8 +109,9 @@
                           ("tag name", "a"),
                           ("xpath", "//*[name()='a']")])
 def test_xhtml_namespace(session, using, value):
-    session.url = inline("""<p><a href="#" id="linkText">full link text</a></p>""", doctype="xhtml")
-    expected = session.execute_script("return document.links[0]")
+    session.url = inline("""<a href="#" id="linkText">full link text</a>""",
+                         doctype="xhtml")
+    expected = session.execute_script("return document.links[0];")
 
     response = find_elements(session, using, value)
     value = assert_success(response)
diff --git a/WebDriverTests/imported/w3c/webdriver/tests/element_retrieval/find_elements_from_element.py b/WebDriverTests/imported/w3c/webdriver/tests/element_retrieval/find_elements_from_element.py
index 486d7cb..4dd8a4a 100644
--- a/WebDriverTests/imported/w3c/webdriver/tests/element_retrieval/find_elements_from_element.py
+++ b/WebDriverTests/imported/w3c/webdriver/tests/element_retrieval/find_elements_from_element.py
@@ -48,6 +48,53 @@
     assert_success(response)
 
 
+@pytest.mark.parametrize("document,value", [
+    ("<a href=#>link text</a>", "link text"),
+    ("<a href=#>&nbsp;link text&nbsp;</a>", "link text"),
+    ("<a href=#>link<br>text</a>", "link\ntext"),
+    ("<a href=#>link&amp;text</a>", "link&text"),
+    ("<a href=#>LINK TEXT</a>", "LINK TEXT"),
+    ("<a href=# style='text-transform: uppercase'>link text</a>", "LINK TEXT"),
+])
+def test_find_elements_link_text(session, document, value):
+    # Step 8 - 9
+    session.url = inline("<div><a href=#>not wanted</a><br/>{0}</div>".format(document))
+    element = session.find.css("div", all=False)
+    expected = session.execute_script("return document.links[1];")
+
+    response = find_elements(session, element.id, "link text", value)
+    value = assert_success(response)
+    assert isinstance(value, list)
+    assert len(value) == 1
+
+    found_element = value[0]
+    assert_same_element(session, found_element, expected)
+
+
+@pytest.mark.parametrize("document,value", [
+    ("<a href=#>partial link text</a>", "link"),
+    ("<a href=#>&nbsp;partial link text&nbsp;</a>", "link"),
+    ("<a href=#>partial link text</a>", "k t"),
+    ("<a href=#>partial link<br>text</a>", "k\nt"),
+    ("<a href=#>partial link&amp;text</a>", "k&t"),
+    ("<a href=#>PARTIAL LINK TEXT</a>", "LINK"),
+    ("<a href=# style='text-transform: uppercase'>partial link text</a>", "LINK"),
+])
+def test_find_elements_partial_link_text(session, document, value):
+    # Step 8 - 9
+    session.url = inline("<div><a href=#>not wanted</a><br/>{0}</div>".format(document))
+    element = session.find.css("div", all=False)
+    expected = session.execute_script("return document.links[1];")
+
+    response = find_elements(session, element.id, "partial link text", value)
+    value = assert_success(response)
+    assert isinstance(value, list)
+    assert len(value) == 1
+
+    found_element = value[0]
+    assert_same_element(session, found_element, expected)
+
+
 @pytest.mark.parametrize("using,value", [("css selector", "#wontExist")])
 def test_no_element(session, using, value):
     # Step 8 - 9
@@ -64,7 +111,8 @@
                           ("tag name", "a"),
                           ("xpath", "//*[name()='a']")])
 def test_xhtml_namespace(session, using, value):
-    session.url = inline("""<p><a href="#" id="linkText">full link text</a></p>""", doctype="xhtml")
+    session.url = inline("""<p><a href="#" id="linkText">full link text</a></p>""",
+                         doctype="xhtml")
     from_element = session.execute_script("""return document.querySelector("p")""")
     expected = session.execute_script("return document.links[0]")
 
@@ -79,9 +127,21 @@
 
 def test_parent_htmldocument(session):
     session.url = inline("")
-    from_element = session.execute_script("return document.documentElement")
+    from_element = session.execute_script("""return document.querySelector("body")""")
+    expected = session.execute_script("return document.documentElement")
 
     response = find_elements(session, from_element.id, "xpath", "..")
     value = assert_success(response)
     assert isinstance(value, list)
     assert len(value) == 1
+
+    found_element = value[0]
+    assert_same_element(session, found_element, expected)
+
+
+def test_parent_of_document_node_errors(session):
+    session.url = inline("")
+    from_element = session.execute_script("return document.documentElement")
+
+    response = find_elements(session, from_element.id, "xpath", "..")
+    assert_error(response, "invalid selector")
diff --git a/WebDriverTests/imported/w3c/webdriver/tests/element_send_keys/form_controls.py b/WebDriverTests/imported/w3c/webdriver/tests/element_send_keys/form_controls.py
index 855d662..28df05f 100644
--- a/WebDriverTests/imported/w3c/webdriver/tests/element_send_keys/form_controls.py
+++ b/WebDriverTests/imported/w3c/webdriver/tests/element_send_keys/form_controls.py
@@ -20,10 +20,10 @@
 
 def add_event_listeners(element):
     element.session.execute_script("""
-        let [target] = arguments;
         window.events = [];
-        for (let expected of ["focus", "change", "keypress", "keydown", "keyup", "input"]) {
-          target.addEventListener(expected, ({type}) => window.events.push(type));
+        var trackedEvents = ["focus", "change", "keypress", "keydown", "keyup", "input"];
+        for (var i = 0; i < trackedEvents.length; i++) {
+          arguments[0].addEventListener(trackedEvents[i], function(eventObject) { window.events.push(eventObject.type) });
         }
         """, args=(element,))
 
diff --git a/WebDriverTests/imported/w3c/webdriver/tests/execute_async_script/collections.py b/WebDriverTests/imported/w3c/webdriver/tests/execute_async_script/collections.py
new file mode 100644
index 0000000..5affdc9
--- /dev/null
+++ b/WebDriverTests/imported/w3c/webdriver/tests/execute_async_script/collections.py
@@ -0,0 +1,158 @@
+import os
+
+from tests.support.asserts import assert_same_element, assert_success
+from tests.support.inline import inline
+
+
+def execute_async_script(session, script, args=None):
+    if args is None:
+        args = []
+    body = {"script": script, "args": args}
+    return session.transport.send(
+        "POST",
+        "/session/{session_id}/execute/async".format(**vars(session)),
+        body)
+
+
+def test_arguments(session):
+    response = execute_async_script(session, """
+        let [resolve] = arguments;
+        function func() {
+            return arguments;
+        }
+        resolve(func("foo", "bar"));
+        """)
+    assert_success(response, [u"foo", u"bar"])
+
+
+def test_array(session):
+    response = execute_async_script(session, """
+        let [resolve] = arguments;
+        resolve([1, 2]);
+        """)
+    assert_success(response, [1, 2])
+
+
+def test_file_list(session, tmpdir):
+    files = [tmpdir.join("foo.txt"), tmpdir.join("bar.txt")]
+
+    session.url = inline("<input type=file multiple>")
+    upload = session.find.css("input", all=False)
+    for file in files:
+        file.write("morn morn")
+        upload.send_keys(str(file))
+
+    response = execute_async_script(session, """
+        let [resolve] = arguments;
+        resolve(document.querySelector('input').files);
+        """)
+    value = assert_success(response)
+    assert isinstance(value, list)
+    assert len(value) == len(files)
+    for expected, actual in zip(files, value):
+        assert isinstance(actual, dict)
+        assert "name" in actual
+        assert isinstance(actual["name"], basestring)
+        assert os.path.basename(str(expected)) == actual["name"]
+
+
+def test_html_all_collection(session):
+    session.url = inline("""
+        <p>foo
+        <p>bar
+        """)
+    html = session.find.css("html", all=False)
+    head = session.find.css("head", all=False)
+    body = session.find.css("body", all=False)
+    ps = session.find.css("p")
+
+    response = execute_async_script(session, """
+        let [resolve] = arguments;
+        resolve(document.all);
+        """)
+    value = assert_success(response)
+    assert isinstance(value, list)
+    # <html>, <head>, <body>, <p>, <p>
+    assert len(value) == 5
+
+    assert_same_element(session, html, value[0])
+    assert_same_element(session, head, value[1])
+    assert_same_element(session, body, value[2])
+    assert_same_element(session, ps[0], value[3])
+    assert_same_element(session, ps[1], value[4])
+
+
+def test_html_collection(session):
+    session.url = inline("""
+        <p>foo
+        <p>bar
+        """)
+    ps = session.find.css("p")
+
+    response = execute_async_script(session, """
+        let [resolve] = arguments;
+        resolve(document.getElementsByTagName('p'));
+        """)
+    value = assert_success(response)
+    assert isinstance(value, list)
+    assert len(value) == 2
+    for expected, actual in zip(ps, value):
+        assert_same_element(session, expected, actual)
+
+
+def test_html_form_controls_collection(session):
+    session.url = inline("""
+        <form>
+            <input>
+            <input>
+        </form>
+        """)
+    inputs = session.find.css("input")
+
+    response = execute_async_script(session, """
+        let [resolve] = arguments;
+        resolve(document.forms[0].elements);
+        """)
+    value = assert_success(response)
+    assert isinstance(value, list)
+    assert len(value) == 2
+    for expected, actual in zip(inputs, value):
+        assert_same_element(session, expected, actual)
+
+
+def test_html_options_collection(session):
+    session.url = inline("""
+        <select>
+            <option>
+            <option>
+        </select>
+        """)
+    options = session.find.css("option")
+
+    response = execute_async_script(session, """
+        let [resolve] = arguments;
+        resolve(document.querySelector('select').options);
+        """)
+    value = assert_success(response)
+    assert isinstance(value, list)
+    assert len(value) == 2
+    for expected, actual in zip(options, value):
+        assert_same_element(session, expected, actual)
+
+
+def test_node_list(session):
+    session.url = inline("""
+        <p>foo
+        <p>bar
+        """)
+    ps = session.find.css("p")
+
+    response = execute_async_script(session, """
+        let [resolve] = arguments;
+        resolve(document.querySelectorAll('p'));
+        """)
+    value = assert_success(response)
+    assert isinstance(value, list)
+    assert len(value) == 2
+    for expected, actual in zip(ps, value):
+        assert_same_element(session, expected, actual)
diff --git a/WebDriverTests/imported/w3c/webdriver/tests/execute_async_script/user_prompts.py b/WebDriverTests/imported/w3c/webdriver/tests/execute_async_script/user_prompts.py
index 03e1762..67f5512 100644
--- a/WebDriverTests/imported/w3c/webdriver/tests/execute_async_script/user_prompts.py
+++ b/WebDriverTests/imported/w3c/webdriver/tests/execute_async_script/user_prompts.py
@@ -7,52 +7,66 @@
 
 def test_handle_prompt_accept(new_session, add_browser_capabilites):
     _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({"unhandledPromptBehavior": "accept"})}})
-    session.execute_async_script("window.alert('Hello');")
+    value = session.execute_async_script("window.alert('Hello');")
+    assert value is None
+    title = session.title
     with pytest.raises(error.NoSuchAlertException):
         session.alert.accept()
 
 
 def test_handle_prompt_dismiss(new_session, add_browser_capabilites):
     _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({"unhandledPromptBehavior": "dismiss"})}})
-    session.execute_async_script("window.alert('Hello');")
+    value = session.execute_async_script("window.alert('Hello');")
+    assert value is None
+    title = session.title
     with pytest.raises(error.NoSuchAlertException):
         session.alert.dismiss()
 
 
 def test_handle_prompt_dismiss_and_notify(new_session, add_browser_capabilites):
     _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({"unhandledPromptBehavior": "dismiss and notify"})}})
+    value = session.execute_async_script("window.alert('Hello');")
+    assert value is None
     with pytest.raises(error.UnexpectedAlertOpenException):
-        session.execute_async_script("window.alert('Hello');")
+        title = session.title
     with pytest.raises(error.NoSuchAlertException):
         session.alert.dismiss()
 
 
 def test_handle_prompt_accept_and_notify(new_session, add_browser_capabilites):
     _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({"unhandledPromptBehavior": "accept and notify"})}})
+    value = session.execute_async_script("window.alert('Hello');")
+    assert value is None
     with pytest.raises(error.UnexpectedAlertOpenException):
-        session.execute_async_script("window.alert('Hello');")
+        title = session.title
     with pytest.raises(error.NoSuchAlertException):
         session.alert.accept()
 
 
 def test_handle_prompt_ignore(new_session, add_browser_capabilites):
     _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({"unhandledPromptBehavior": "ignore"})}})
+    value = session.execute_async_script("window.alert('Hello');")
+    assert value is None
     with pytest.raises(error.UnexpectedAlertOpenException):
-        session.execute_async_script("window.alert('Hello');")
+        title = session.title
     session.alert.dismiss()
 
 
 def test_handle_prompt_default(new_session, add_browser_capabilites):
     _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({})}})
+    value = session.execute_async_script("window.alert('Hello');")
+    assert value is None
     with pytest.raises(error.UnexpectedAlertOpenException):
-        session.execute_async_script("window.alert('Hello');")
+        title = session.title
     with pytest.raises(error.NoSuchAlertException):
         session.alert.dismiss()
 
 
 def test_handle_prompt_twice(new_session, add_browser_capabilites):
     _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({"unhandledPromptBehavior": "accept"})}})
-    session.execute_async_script("window.alert('Hello');window.alert('Bye');")
+    value = session.execute_async_script("window.alert('Hello');window.alert('Bye');")
+    assert value is None
+    session.alert.dismiss()
     # The first alert has been accepted by the user prompt handler, the second one remains.
     # FIXME: this is how browsers currently work, but the spec should clarify if this is the
     #        expected behavior, see https://github.com/w3c/webdriver/issues/1153.
diff --git a/WebDriverTests/imported/w3c/webdriver/tests/execute_script/collections.py b/WebDriverTests/imported/w3c/webdriver/tests/execute_script/collections.py
new file mode 100644
index 0000000..d96c7fe
--- /dev/null
+++ b/WebDriverTests/imported/w3c/webdriver/tests/execute_script/collections.py
@@ -0,0 +1,136 @@
+import os
+
+from tests.support.asserts import assert_same_element, assert_success
+from tests.support.inline import inline
+
+
+def execute_script(session, script, args=None):
+    if args is None:
+        args = []
+    body = {"script": script, "args": args}
+    return session.transport.send(
+        "POST",
+        "/session/{session_id}/execute/sync".format(**vars(session)),
+        body)
+
+
+def test_arguments(session):
+    response = execute_script(session, """
+        function func() {
+            return arguments;
+        }
+        return func("foo", "bar");
+        """)
+    assert_success(response, [u"foo", u"bar"])
+
+
+def test_array(session):
+    response = execute_script(session, "return [1, 2]")
+    assert_success(response, [1, 2])
+
+
+def test_file_list(session, tmpdir):
+    files = [tmpdir.join("foo.txt"), tmpdir.join("bar.txt")]
+
+    session.url = inline("<input type=file multiple>")
+    upload = session.find.css("input", all=False)
+    for file in files:
+        file.write("morn morn")
+        upload.send_keys(str(file))
+
+    response = execute_script(session, "return document.querySelector('input').files")
+    value = assert_success(response)
+    assert isinstance(value, list)
+    assert len(value) == len(files)
+    for expected, actual in zip(files, value):
+        assert isinstance(actual, dict)
+        assert "name" in actual
+        assert isinstance(actual["name"], basestring)
+        assert os.path.basename(str(expected)) == actual["name"]
+
+
+def test_html_all_collection(session):
+    session.url = inline("""
+        <p>foo
+        <p>bar
+        """)
+    html = session.find.css("html", all=False)
+    head = session.find.css("head", all=False)
+    body = session.find.css("body", all=False)
+    ps = session.find.css("p")
+
+    response = execute_script(session, "return document.all")
+    value = assert_success(response)
+    assert isinstance(value, list)
+    # <html>, <head>, <body>, <p>, <p>
+    assert len(value) == 5
+
+    assert_same_element(session, html, value[0])
+    assert_same_element(session, head, value[1])
+    assert_same_element(session, body, value[2])
+    assert_same_element(session, ps[0], value[3])
+    assert_same_element(session, ps[1], value[4])
+
+
+def test_html_collection(session):
+    session.url = inline("""
+        <p>foo
+        <p>bar
+        """)
+    ps = session.find.css("p")
+
+    response = execute_script(session, "return document.getElementsByTagName('p')")
+    value = assert_success(response)
+    assert isinstance(value, list)
+    assert len(value) == 2
+    for expected, actual in zip(ps, value):
+        assert_same_element(session, expected, actual)
+
+
+def test_html_form_controls_collection(session):
+    session.url = inline("""
+        <form>
+            <input>
+            <input>
+        </form>
+        """)
+    inputs = session.find.css("input")
+
+    response = execute_script(session, "return document.forms[0].elements")
+    value = assert_success(response)
+    assert isinstance(value, list)
+    assert len(value) == 2
+    for expected, actual in zip(inputs, value):
+        assert_same_element(session, expected, actual)
+
+
+def test_html_options_collection(session):
+    session.url = inline("""
+        <select>
+            <option>
+            <option>
+        </select>
+        """)
+    options = session.find.css("option")
+
+    response = execute_script(session, "return document.querySelector('select').options")
+    value = assert_success(response)
+    assert isinstance(value, list)
+    assert len(value) == 2
+    for expected, actual in zip(options, value):
+        assert_same_element(session, expected, actual)
+
+
+def test_node_list(session):
+    session.url = inline("""
+        <p>foo
+        <p>bar
+        """)
+    ps = session.find.css("p")
+
+    response = execute_script(session, "return document.querySelectorAll('p')")
+    value = assert_success(response)
+    assert isinstance(value, list)
+    assert len(value) == 2
+    for expected, actual in zip(ps, value):
+        assert_same_element(session, expected, actual)
diff --git a/WebDriverTests/imported/w3c/webdriver/tests/execute_script/user_prompts.py b/WebDriverTests/imported/w3c/webdriver/tests/execute_script/user_prompts.py
index 8d91bdd..befa8d8 100644
--- a/WebDriverTests/imported/w3c/webdriver/tests/execute_script/user_prompts.py
+++ b/WebDriverTests/imported/w3c/webdriver/tests/execute_script/user_prompts.py
@@ -7,52 +7,66 @@
 
 def test_handle_prompt_accept(new_session, add_browser_capabilites):
     _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({"unhandledPromptBehavior": "accept"})}})
-    session.execute_script("window.alert('Hello');")
+    value = session.execute_script("window.alert('Hello');")
+    assert value is None
+    title = session.title
     with pytest.raises(error.NoSuchAlertException):
         session.alert.accept()
 
 
 def test_handle_prompt_dismiss(new_session, add_browser_capabilites):
     _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({"unhandledPromptBehavior": "dismiss"})}})
-    session.execute_script("window.alert('Hello');")
+    value = session.execute_script("window.alert('Hello');")
+    assert value is None
+    title = session.title
     with pytest.raises(error.NoSuchAlertException):
         session.alert.dismiss()
 
 
 def test_handle_prompt_dismiss_and_notify(new_session, add_browser_capabilites):
     _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({"unhandledPromptBehavior": "dismiss and notify"})}})
+    value = session.execute_script("window.alert('Hello');")
+    assert value is None
     with pytest.raises(error.UnexpectedAlertOpenException):
-        session.execute_script("window.alert('Hello');")
+        title = session.title
     with pytest.raises(error.NoSuchAlertException):
         session.alert.dismiss()
 
 
 def test_handle_prompt_accept_and_notify(new_session, add_browser_capabilites):
     _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({"unhandledPromptBehavior": "accept and notify"})}})
+    value = session.execute_script("window.alert('Hello');")
+    assert value is None
     with pytest.raises(error.UnexpectedAlertOpenException):
-        session.execute_script("window.alert('Hello');")
+        title = session.title
     with pytest.raises(error.NoSuchAlertException):
         session.alert.accept()
 
 
 def test_handle_prompt_ignore(new_session, add_browser_capabilites):
     _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({"unhandledPromptBehavior": "ignore"})}})
+    value = session.execute_script("window.alert('Hello');")
+    assert value is None
     with pytest.raises(error.UnexpectedAlertOpenException):
-        session.execute_script("window.alert('Hello');")
+        title = session.title
     session.alert.dismiss()
 
 
 def test_handle_prompt_default(new_session, add_browser_capabilites):
     _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({})}})
+    value = session.execute_script("window.alert('Hello');")
+    assert value is None
     with pytest.raises(error.UnexpectedAlertOpenException):
-        session.execute_script("window.alert('Hello');")
+        title = session.title
     with pytest.raises(error.NoSuchAlertException):
         session.alert.dismiss()
 
 
 def test_handle_prompt_twice(new_session, add_browser_capabilites):
     _, session = new_session({"capabilities": {"alwaysMatch": add_browser_capabilites({"unhandledPromptBehavior": "accept"})}})
-    session.execute_script("window.alert('Hello');window.alert('Bye');")
+    value = session.execute_script("window.alert('Hello');window.alert('Bye');")
+    assert value is None
+    session.alert.dismiss()
     # The first alert has been accepted by the user prompt handler, the second one remains.
     # FIXME: this is how browsers currently work, but the spec should clarify if this is the
     #        expected behavior, see https://github.com/w3c/webdriver/issues/1153.
diff --git a/WebDriverTests/imported/w3c/webdriver/tests/fullscreen_window.py b/WebDriverTests/imported/w3c/webdriver/tests/fullscreen_window.py
index 43eecc5..36dd78f 100644
--- a/WebDriverTests/imported/w3c/webdriver/tests/fullscreen_window.py
+++ b/WebDriverTests/imported/w3c/webdriver/tests/fullscreen_window.py
@@ -16,6 +16,12 @@
     return session.transport.send("POST", "session/%s/window/fullscreen" % session.session_id)
 
 
+def is_fullscreen(session):
+    # At the time of writing, WebKit does not conform to the Fullscreen API specification.
+    # Remove the prefixed fallback when https://bugs.webkit.org/show_bug.cgi?id=158125 is fixed.
+    return session.execute_script("return !!(window.fullScreen || document.webkitIsFullScreen)")
+
+
 # 10.7.5 Fullscreen Window
 
 
@@ -135,11 +141,6 @@
     assert read_global(session, "dismiss3") == None
 
 
-def is_fullscreen(session):
-    # At the time of writing, WebKit does not conform to the Fullscreen API specification.
-    # Remove the prefixed fallback when https://bugs.webkit.org/show_bug.cgi?id=158125 is fixed.
-    return session.execute_script("return !!(window.fullScreen || document.webkitIsFullScreen)")
-
 def test_fullscreen(session):
     """
     4. Call fullscreen an element with the current top-level browsing
diff --git a/WebDriverTests/imported/w3c/webdriver/tests/interaction/element_clear.py b/WebDriverTests/imported/w3c/webdriver/tests/interaction/element_clear.py
index 0443995..ceec0e2 100644
--- a/WebDriverTests/imported/w3c/webdriver/tests/interaction/element_clear.py
+++ b/WebDriverTests/imported/w3c/webdriver/tests/interaction/element_clear.py
@@ -9,13 +9,13 @@
 )
 from tests.support.inline import inline
 
-
 def add_event_listeners(element):
     element.session.execute_script("""
-        let [target] = arguments;
+        var target = arguments[0];
         window.events = [];
-        for (let expected of ["focus", "blur", "change"]) {
-          target.addEventListener(expected, ({type}) => window.events.push(type));
+        var expectedEvents = ["focus", "blur", "change"];
+        for (var i = 0; i < expectedEvents.length; i++) {
+          target.addEventListener(expectedEvents[i], function (eventObject) { window.events.push(eventObject.type) });
         }
         """, args=(element,))
 
@@ -272,7 +272,6 @@
     assert_element_has_focus(session.execute_script("return document.body"))
 
 
-
 def test_designmode(session):
     session.url = inline("foobar")
     element = session.find.css("body", all=False)
@@ -323,7 +322,7 @@
 
     def is_valid(element):
         return session.execute_script("""
-            let [input] = arguments;
+            var input = arguments[0];
             return input.validity.valid;
             """, args=(element,))
 
@@ -361,12 +360,12 @@
         """)
     element = session.find.css("input", all=False)
     assert element.property("value") == "foobar"
-    assert session.execute_script("return window.scrollY") == 0
+    assert session.execute_script("return window.pageYOffset") == 0
 
     # scroll to the bottom right of the page
     session.execute_script("""
-        let {scrollWidth, scrollHeight} = document.body;
-        window.scrollTo(scrollWidth, scrollHeight);
+        var body = document.body;
+        window.scrollTo(body.scrollWidth, body.scrollHeight);
         """)
 
     # clear and scroll back to the top of the page
@@ -376,12 +375,18 @@
 
     # check if element cleared is scrolled into view
     rect = session.execute_script("""
-        let [input] = arguments;
-        return input.getBoundingClientRect();
+        var input = arguments[0];
+        var rect = input.getBoundingClientRect();
+        return {"top": rect.top,
+                "left": rect.left,
+                "height": rect.height,
+                "width": rect.width};
         """, args=(element,))
     window = session.execute_script("""
-        let {innerHeight, innerWidth, pageXOffset, pageYOffset} = window;
-        return {innerHeight, innerWidth, pageXOffset, pageYOffset};
+        return {"innerHeight": window.innerHeight,
+                "innerWidth": window.innerWidth,
+                "pageXOffset": window.pageXOffset,
+                "pageYOffset": window.pageYOffset};
         """)
 
     assert rect["top"] < (window["innerHeight"] + window["pageYOffset"]) and \
diff --git a/WebDriverTests/imported/w3c/webdriver/tests/minimize_window.py b/WebDriverTests/imported/w3c/webdriver/tests/minimize_window.py
index 5c0ef72..ff451de 100644
--- a/WebDriverTests/imported/w3c/webdriver/tests/minimize_window.py
+++ b/WebDriverTests/imported/w3c/webdriver/tests/minimize_window.py
@@ -10,6 +10,11 @@
     return session.transport.send("POST", "session/%s/window/minimize" % session.session_id)
 
 
+def is_fullscreen(session):
+    # At the time of writing, WebKit does not conform to the Fullscreen API specification.
+    # Remove the prefixed fallback when https://bugs.webkit.org/show_bug.cgi?id=158125 is fixed.
+    return session.execute_script("return !!(window.fullScreen || document.webkitIsFullScreen)")
+
 # 10.7.4 Minimize Window
 
 
@@ -135,11 +140,11 @@
 
     """
     session.window.fullscreen()
-    assert session.execute_script("return window.fullScreen") is True
+    assert is_fullscreen(session) is True
 
     response = minimize(session)
     assert_success(response)
-    assert session.execute_script("return window.fullScreen") is False
+    assert is_fullscreen(session) is False
     assert session.execute_script("return document.hidden") is True
 
 
diff --git a/WebDriverTests/imported/w3c/webdriver/tests/set_window_rect.py b/WebDriverTests/imported/w3c/webdriver/tests/set_window_rect.py
index f945f19..d032b9b 100644
--- a/WebDriverTests/imported/w3c/webdriver/tests/set_window_rect.py
+++ b/WebDriverTests/imported/w3c/webdriver/tests/set_window_rect.py
@@ -13,6 +13,11 @@
 def set_window_rect(session, rect):
     return session.transport.send("POST", "session/%s/window/rect" % session.session_id, rect)
 
+def is_fullscreen(session):
+    # At the time of writing, WebKit does not conform to the Fullscreen API specification.
+    # Remove the prefixed fallback when https://bugs.webkit.org/show_bug.cgi?id=158125 is fixed.
+    return session.execute_script("return !!(window.fullScreen || document.webkitIsFullScreen)")
+
 
 # 10.7.2 Set Window Rect
 
@@ -293,14 +298,14 @@
       3. Exit fullscreen document.
     """
     session.window.fullscreen()
-    assert session.execute_script("return window.fullScreen") is True
+    assert is_fullscreen(session) is True
 
     response = set_window_rect(session, {"width": 400, "height": 400})
     value = assert_success(response)
     assert value["width"] == 400
     assert value["height"] == 400
 
-    assert session.execute_script("return window.fullScreen") is False
+    assert is_fullscreen(session) is False
 
 
 def test_restore_from_minimized(session):
diff --git a/WebDriverTests/imported/w3c/webdriver/tests/support/fixtures.py b/WebDriverTests/imported/w3c/webdriver/tests/support/fixtures.py
index 844ae03..6a20fec 100644
--- a/WebDriverTests/imported/w3c/webdriver/tests/support/fixtures.py
+++ b/WebDriverTests/imported/w3c/webdriver/tests/support/fixtures.py
@@ -1,28 +1,26 @@
+from __future__ import print_function
+
 import json
 import os
 import urlparse
 import re
+import sys
 
 import webdriver
-import mozlog
 
-from tests.support.asserts import assert_error
 from tests.support.http_request import HTTPRequest
 from tests.support.wait import wait
-from tests.support import merge_dictionaries
 
 default_host = "http://127.0.0.1"
 default_port = "4444"
 
-logger = mozlog.get_default_logger()
-
 
 def ignore_exceptions(f):
     def inner(*args, **kwargs):
         try:
             return f(*args, **kwargs)
         except webdriver.error.WebDriverException as e:
-            logger.warning("Ignored exception %s" % e)
+            print("Ignored exception %s" % e, file=sys.stderr)
     inner.__name__ = f.__name__
     return inner
 
@@ -182,7 +180,8 @@
         global _current_session
         if _current_session is not None and _current_session.session_id:
             _current_session.end()
-            _current_session = None
+
+        _current_session = None
 
     def create_session(body):
         global _current_session
@@ -215,7 +214,7 @@
 def url(server_config):
     def inner(path, protocol="http", query="", fragment=""):
         port = server_config["ports"][protocol][0]
-        host = "%s:%s" % (server_config["host"], port)
+        host = "%s:%s" % (server_config["browser_host"], port)
         return urlparse.urlunsplit((protocol, host, path, query, fragment))
 
     inner.__name__ = "url"