summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/resources/test
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /testing/web-platform/tests/resources/test
parentInitial commit. (diff)
downloadthunderbird-upstream.tar.xz
thunderbird-upstream.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--testing/web-platform/tests/resources/test-only-api.js31
-rw-r--r--testing/web-platform/tests/resources/test-only-api.js.headers2
-rw-r--r--testing/web-platform/tests/resources/test-only-api.m.js5
-rw-r--r--testing/web-platform/tests/resources/test-only-api.m.js.headers2
-rw-r--r--testing/web-platform/tests/resources/test/README.md83
-rw-r--r--testing/web-platform/tests/resources/test/conftest.py269
-rw-r--r--testing/web-platform/tests/resources/test/harness.html26
-rw-r--r--testing/web-platform/tests/resources/test/idl-helper.js24
-rw-r--r--testing/web-platform/tests/resources/test/nested-testharness.js80
-rw-r--r--testing/web-platform/tests/resources/test/requirements.txt1
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/abortsignal.html49
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/add_cleanup.html91
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async.html85
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_bad_return.html50
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_rejection.html94
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_rejection_after_load.html52
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_timeout.html57
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/add_cleanup_bad_return.html61
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/add_cleanup_count.html39
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/add_cleanup_err.html45
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/add_cleanup_err_multi.html52
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/add_cleanup_sync_queue.html55
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/api-tests-1.html991
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/api-tests-2.html62
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/api-tests-3.html34
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/assert-array-equals.html162
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/assert-throws-dom.html55
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/force_timeout.html60
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/generate-callback.html153
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlDictionary/test_partial_interface_of.html90
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_exposed_wildcard.html233
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_immutable_prototype.html298
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_interface_mixin.html131
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_partial_interface_of.html188
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_primary_interface_of.html116
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_to_json_operation.html177
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_attribute.html100
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_operation.html242
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_partial_namespace.html125
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/iframe-callback.html116
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/iframe-consolidate-errors.html50
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/iframe-consolidate-tests.html85
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/iframe-msg.html84
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/log-insertion.html46
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/no-title.html146
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/order.html36
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/promise-async.html172
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/promise-with-sync.html79
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/promise.html219
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/queue.html130
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/setup-function-worker.js14
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/setup-worker-service.html86
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/single-page-test-fail.html28
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/single-page-test-no-assertions.html25
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/single-page-test-no-body.html26
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/single-page-test-pass.html28
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/step_wait.html57
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/step_wait_func.html49
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/task-scheduling-promise-test.html241
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/task-scheduling-test.html141
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/uncaught-exception-handle.html33
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/uncaught-exception-ignore.html35
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/worker-dedicated-uncaught-allow.html45
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/worker-dedicated-uncaught-single.html56
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/worker-dedicated.sub.html88
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/worker-error.js8
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/worker-service.html115
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/worker-shared.html73
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/worker-uncaught-allow.js19
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/worker-uncaught-single.js8
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/worker.js34
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlArray/is_json_type.html192
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlDictionary/get_reverse_inheritance_stack.html47
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlDictionary/test_partial_dictionary.html39
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlInterface/constructors.html26
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlInterface/default_to_json_operation.html114
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlInterface/do_member_unscopable_asserts.html56
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_interface_object.html22
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_interface_object_owner.html21
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_legacy_namespace.html20
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_qualified_name.html20
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_reverse_inheritance_stack.html49
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlInterface/has_default_to_json_regular_operation.html47
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlInterface/has_to_json_regular_operation.html31
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlInterface/should_have_interface_object.html30
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlInterface/test_primary_interface_of_undefined.html29
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlInterfaceMember/is_to_json_regular_operation.html42
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlInterfaceMember/toString.html36
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/assert_implements.html43
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/assert_implements_optional.html43
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/assert_object_equals.html152
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/async-test-return-restrictions.html135
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/basic.html48
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/exceptional-cases-timeouts.html120
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/exceptional-cases.html392
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/format-value.html123
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/helpers.js21
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/late-test.html54
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/promise_setup-timeout.html28
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/promise_setup.html333
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/single_test.html94
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/test-return-restrictions.html156
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/throwing-assertions.html268
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/unpaired-surrogates.html143
-rw-r--r--testing/web-platform/tests/resources/test/tox.ini13
-rw-r--r--testing/web-platform/tests/resources/test/wptserver.py58
-rw-r--r--testing/web-platform/tests/resources/testdriver-actions.js599
-rw-r--r--testing/web-platform/tests/resources/testdriver-vendor.js0
-rw-r--r--testing/web-platform/tests/resources/testdriver-vendor.js.headers2
-rw-r--r--testing/web-platform/tests/resources/testdriver.js809
-rw-r--r--testing/web-platform/tests/resources/testdriver.js.headers2
-rw-r--r--testing/web-platform/tests/resources/testharness.js4933
-rw-r--r--testing/web-platform/tests/resources/testharness.js.headers2
-rw-r--r--testing/web-platform/tests/resources/testharnessreport.js57
-rw-r--r--testing/web-platform/tests/resources/testharnessreport.js.headers2
115 files changed, 16293 insertions, 0 deletions
diff --git a/testing/web-platform/tests/resources/test-only-api.js b/testing/web-platform/tests/resources/test-only-api.js
new file mode 100644
index 0000000000..a66eb44ede
--- /dev/null
+++ b/testing/web-platform/tests/resources/test-only-api.js
@@ -0,0 +1,31 @@
+'use strict';
+
+/* Whether the browser is Chromium-based with MojoJS enabled */
+const isChromiumBased = 'MojoInterfaceInterceptor' in self;
+/* Whether the browser is WebKit-based with internal test-only API enabled */
+const isWebKitBased = !isChromiumBased && 'internals' in self;
+
+/**
+ * Loads a script in a window or worker.
+ *
+ * @param {string} path - A script path
+ * @returns {Promise}
+ */
+function loadScript(path) {
+ if (typeof document === 'undefined') {
+ // Workers (importScripts is synchronous and may throw.)
+ importScripts(path);
+ return Promise.resolve();
+ } else {
+ // Window
+ const script = document.createElement('script');
+ script.src = path;
+ script.async = false;
+ const p = new Promise((resolve, reject) => {
+ script.onload = () => { resolve(); };
+ script.onerror = e => { reject(`Error loading ${path}`); };
+ })
+ document.head.appendChild(script);
+ return p;
+ }
+}
diff --git a/testing/web-platform/tests/resources/test-only-api.js.headers b/testing/web-platform/tests/resources/test-only-api.js.headers
new file mode 100644
index 0000000000..5e8f640c66
--- /dev/null
+++ b/testing/web-platform/tests/resources/test-only-api.js.headers
@@ -0,0 +1,2 @@
+Content-Type: text/javascript; charset=utf-8
+Cache-Control: max-age=3600
diff --git a/testing/web-platform/tests/resources/test-only-api.m.js b/testing/web-platform/tests/resources/test-only-api.m.js
new file mode 100644
index 0000000000..984f635aba
--- /dev/null
+++ b/testing/web-platform/tests/resources/test-only-api.m.js
@@ -0,0 +1,5 @@
+/* Whether the browser is Chromium-based with MojoJS enabled */
+export const isChromiumBased = 'MojoInterfaceInterceptor' in self;
+
+/* Whether the browser is WebKit-based with internal test-only API enabled */
+export const isWebKitBased = !isChromiumBased && 'internals' in self;
diff --git a/testing/web-platform/tests/resources/test-only-api.m.js.headers b/testing/web-platform/tests/resources/test-only-api.m.js.headers
new file mode 100644
index 0000000000..5e8f640c66
--- /dev/null
+++ b/testing/web-platform/tests/resources/test-only-api.m.js.headers
@@ -0,0 +1,2 @@
+Content-Type: text/javascript; charset=utf-8
+Cache-Control: max-age=3600
diff --git a/testing/web-platform/tests/resources/test/README.md b/testing/web-platform/tests/resources/test/README.md
new file mode 100644
index 0000000000..edc03ef214
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/README.md
@@ -0,0 +1,83 @@
+# `testharness.js` test suite
+
+The test suite for the `testharness.js` testing framework.
+
+## Executing Tests
+
+Install the following dependencies:
+
+- [Python 2.7.9+](https://www.python.org/)
+- [the tox Python package](https://tox.readthedocs.io/en/latest/)
+- [the Mozilla Firefox web browser](https://mozilla.org/firefox)
+- [the GeckoDriver server](https://github.com/mozilla/geckodriver)
+
+Make sure `geckodriver` can be found in your `PATH`.
+
+Currently, the tests should be run with the latest *Firefox Nightly*. In order to
+specify the path to Firefox Nightly, use the following command-line option:
+
+ tox -- --binary=/path/to/FirefoxNightly
+
+### Automated Script
+
+Alternatively, you may run `tools/ci/ci_resources_unittest.sh`, which only depends on
+Python 2. The script will install other dependencies automatically and start `tox` with
+the correct arguments.
+
+## Authoring Tests
+
+Test cases are expressed as `.html` files located within the `tests/unit/` and
+`tests/functional/` sub-directories. Each test should include the
+`testharness.js` library with the following markup:
+
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+
+This should be followed by one or more `<script>` tags that interface with the
+`testharness.js` API in some way. For example:
+
+ <script>
+ test(function() {
+ 1 = 1;
+ }, 'This test is expected to fail.');
+ </script>
+
+### Unit tests
+
+The "unit test" type allows for concisely testing the expected behavior of
+assertion methods. These tests may define any number of sub-tests; the
+acceptance criteria is simply that all tests executed pass.
+
+### Functional tests
+
+Thoroughly testing the behavior of the harness itself requires ensuring a
+number of considerations which cannot be verified with the "unit testing"
+strategy. These include:
+
+- Ensuring that some tests are not run
+- Ensuring conditions that cause test failures
+- Ensuring conditions that cause harness errors
+
+Functional tests allow for these details to be verified. Every functional test
+must include a summary of the expected results as a JSON string within a
+`<script>` tag with an `id` of `"expected"`, e.g.:
+
+ <script type="text/json" id="expected">
+ {
+ "summarized_status": {
+ "message": null,
+ "stack": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "message": "ReferenceError: invalid assignment left-hand side",
+ "name": "Sample HTML5 API Tests",
+ "properties": {},
+ "stack": "(implementation-defined)",
+ "status_string": "FAIL"
+ }
+ ],
+ "type": "complete"
+ }
+ </script>
diff --git a/testing/web-platform/tests/resources/test/conftest.py b/testing/web-platform/tests/resources/test/conftest.py
new file mode 100644
index 0000000000..7253cac9ac
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/conftest.py
@@ -0,0 +1,269 @@
+import copy
+import json
+import os
+import ssl
+import sys
+import subprocess
+import urllib
+
+import html5lib
+import py
+import pytest
+
+from wptserver import WPTServer
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+WPT_ROOT = os.path.normpath(os.path.join(HERE, '..', '..'))
+HARNESS = os.path.join(HERE, 'harness.html')
+TEST_TYPES = ('functional', 'unit')
+
+sys.path.insert(0, os.path.normpath(os.path.join(WPT_ROOT, "tools")))
+import localpaths
+
+sys.path.insert(0, os.path.normpath(os.path.join(WPT_ROOT, "tools", "webdriver")))
+import webdriver
+
+
+def pytest_addoption(parser):
+ parser.addoption("--binary", action="store", default=None, help="path to browser binary")
+ parser.addoption("--headless", action="store_true", default=False, help="run browser in headless mode")
+
+
+def pytest_collect_file(file_path, path, parent):
+ if file_path.suffix.lower() != '.html':
+ return
+
+ # Tests are organized in directories by type
+ test_type = os.path.relpath(str(file_path), HERE)
+ if os.path.sep not in test_type or ".." in test_type:
+ # HTML files in this directory are not tests
+ return
+ test_type = test_type.split(os.path.sep)[1]
+
+ # Handle the deprecation of Node construction in pytest6
+ # https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent
+ if hasattr(HTMLItem, "from_parent"):
+ return HTMLItem.from_parent(parent, filename=str(file_path), test_type=test_type)
+ return HTMLItem(parent, str(file_path), test_type)
+
+
+def pytest_configure(config):
+ config.proc = subprocess.Popen(["geckodriver"])
+ config.add_cleanup(config.proc.kill)
+
+ capabilities = {"alwaysMatch": {"acceptInsecureCerts": True, "moz:firefoxOptions": {}}}
+ if config.getoption("--binary"):
+ capabilities["alwaysMatch"]["moz:firefoxOptions"]["binary"] = config.getoption("--binary")
+ if config.getoption("--headless"):
+ capabilities["alwaysMatch"]["moz:firefoxOptions"]["args"] = ["--headless"]
+
+ config.driver = webdriver.Session("localhost", 4444,
+ capabilities=capabilities)
+ config.add_cleanup(config.driver.end)
+
+ # Although the name of the `_create_unverified_context` method suggests
+ # that it is not intended for external consumption, the standard library's
+ # documentation explicitly endorses its use:
+ #
+ # > To revert to the previous, unverified, behavior
+ # > ssl._create_unverified_context() can be passed to the context
+ # > parameter.
+ #
+ # https://docs.python.org/2/library/httplib.html#httplib.HTTPSConnection
+ config.ssl_context = ssl._create_unverified_context()
+
+ config.server = WPTServer(WPT_ROOT)
+ config.server.start(config.ssl_context)
+ config.add_cleanup(config.server.stop)
+
+
+def resolve_uri(context, uri):
+ if uri.startswith('/'):
+ base = WPT_ROOT
+ path = uri[1:]
+ else:
+ base = os.path.dirname(context)
+ path = uri
+
+ return os.path.exists(os.path.join(base, path))
+
+
+def _summarize(actual):
+ def _scrub_stack(test_obj):
+ copy = dict(test_obj)
+ del copy['stack']
+ return copy
+
+ def _expand_status(status_obj):
+ for key, value in [item for item in status_obj.items()]:
+ # In "status" and "test" objects, the "status" value enum
+ # definitions are interspersed with properties for unrelated
+ # metadata. The following condition is a best-effort attempt to
+ # ignore non-enum properties.
+ if key != key.upper() or not isinstance(value, int):
+ continue
+
+ del status_obj[key]
+
+ if status_obj['status'] == value:
+ status_obj[u'status_string'] = key
+
+ del status_obj['status']
+
+ return status_obj
+
+ def _summarize_test(test_obj):
+ del test_obj['index']
+
+ assert 'phase' in test_obj
+ assert 'phases' in test_obj
+ assert 'COMPLETE' in test_obj['phases']
+ assert test_obj['phase'] == test_obj['phases']['COMPLETE']
+ del test_obj['phases']
+ del test_obj['phase']
+
+ return _expand_status(_scrub_stack(test_obj))
+
+ def _summarize_status(status_obj):
+ return _expand_status(_scrub_stack(status_obj))
+
+
+ summarized = {}
+
+ summarized[u'summarized_status'] = _summarize_status(actual['status'])
+ summarized[u'summarized_tests'] = [
+ _summarize_test(test) for test in actual['tests']]
+ summarized[u'summarized_tests'].sort(key=lambda test_obj: test_obj.get('name'))
+ summarized[u'summarized_asserts'] = [
+ {"assert_name": assert_item["assert_name"],
+ "test": assert_item["test"]["name"] if assert_item["test"] else None,
+ "args": assert_item["args"],
+ "status": assert_item["status"]} for assert_item in actual["asserts"]]
+ summarized[u'type'] = actual['type']
+
+ return summarized
+
+
+class HTMLItem(pytest.Item, pytest.Collector):
+ def __init__(self, parent, filename, test_type):
+ self.url = parent.session.config.server.url(filename)
+ self.type = test_type
+ # Some tests are reliant on the WPT servers substitution functionality,
+ # so tests must be retrieved from the server rather than read from the
+ # file system directly.
+ handle = urllib.request.urlopen(self.url,
+ context=parent.session.config.ssl_context)
+ try:
+ markup = handle.read()
+ finally:
+ handle.close()
+
+ if test_type not in TEST_TYPES:
+ raise ValueError('Unrecognized test type: "%s"' % test_type)
+
+ parsed = html5lib.parse(markup, namespaceHTMLElements=False)
+ name = None
+ self.expected = None
+
+ for element in parsed.iter():
+ if not name and element.tag == 'title':
+ name = element.text
+ continue
+ if element.tag == 'script':
+ if element.attrib.get('id') == 'expected':
+ try:
+ self.expected = json.loads(element.text)
+ except ValueError:
+ print("Failed parsing JSON in %s" % filename)
+ raise
+
+ if not name:
+ raise ValueError('No name found in %s add a <title> element' % filename)
+ elif self.type == 'functional':
+ if not self.expected:
+ raise ValueError('Functional tests must specify expected report data')
+ elif self.type == 'unit' and self.expected:
+ raise ValueError('Unit tests must not specify expected report data')
+
+ # Ensure that distinct items have distinct fspath attributes.
+ # This is necessary because pytest has an internal cache keyed on it,
+ # and only the first test with any given fspath will be run.
+ #
+ # This cannot use super(HTMLItem, self).__init__(..) because only the
+ # Collector constructor takes the fspath argument.
+ pytest.Item.__init__(self, name, parent)
+ pytest.Collector.__init__(self, name, parent, fspath=py.path.local(filename))
+
+
+ def reportinfo(self):
+ return self.fspath, None, self.url
+
+ def repr_failure(self, excinfo):
+ return pytest.Collector.repr_failure(self, excinfo)
+
+ def runtest(self):
+ if self.type == 'unit':
+ self._run_unit_test()
+ elif self.type == 'functional':
+ self._run_functional_test()
+ else:
+ raise NotImplementedError
+
+ def _run_unit_test(self):
+ driver = self.session.config.driver
+ server = self.session.config.server
+
+ driver.url = server.url(HARNESS)
+
+ actual = driver.execute_async_script(
+ 'runTest("%s", "foo", arguments[0])' % self.url
+ )
+
+ summarized = _summarize(copy.deepcopy(actual))
+
+ print(json.dumps(summarized, indent=2))
+
+ assert summarized[u'summarized_status'][u'status_string'] == u'OK', summarized[u'summarized_status'][u'message']
+ for test in summarized[u'summarized_tests']:
+ msg = "%s\n%s" % (test[u'name'], test[u'message'])
+ assert test[u'status_string'] == u'PASS', msg
+
+ def _run_functional_test(self):
+ driver = self.session.config.driver
+ server = self.session.config.server
+
+ driver.url = server.url(HARNESS)
+
+ test_url = self.url
+ actual = driver.execute_async_script('runTest("%s", "foo", arguments[0])' % test_url)
+
+ print(json.dumps(actual, indent=2))
+
+ summarized = _summarize(copy.deepcopy(actual))
+
+ print(json.dumps(summarized, indent=2))
+
+ # Test object ordering is not guaranteed. This weak assertion verifies
+ # that the indices are unique and sequential
+ indices = [test_obj.get('index') for test_obj in actual['tests']]
+ self._assert_sequence(indices)
+
+ self.expected[u'summarized_tests'].sort(key=lambda test_obj: test_obj.get('name'))
+
+ # Make asserts opt-in for now
+ if "summarized_asserts" not in self.expected:
+ del summarized["summarized_asserts"]
+ else:
+ # We can't be sure of the order of asserts even within the same test
+ # although we could also check for the failing assert being the final
+ # one
+ for obj in [summarized, self.expected]:
+ obj["summarized_asserts"].sort(
+ key=lambda x: (x["test"] or "", x["status"], x["assert_name"], tuple(x["args"])))
+
+ assert summarized == self.expected
+
+ @staticmethod
+ def _assert_sequence(nums):
+ if nums and len(nums) > 0:
+ assert nums == list(range(1, nums[-1] + 1))
diff --git a/testing/web-platform/tests/resources/test/harness.html b/testing/web-platform/tests/resources/test/harness.html
new file mode 100644
index 0000000000..5ee0f285e8
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/harness.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ </head>
+ <body>
+ <script>
+function runTest(url, id, done) {
+ var child;
+
+ function onMessage(event) {
+ if (!event.data || event.data.type !== 'complete') {
+ return;
+ }
+
+ window.removeEventListener('message', onMessage);
+ child.close();
+ done(event.data);
+ }
+ window.addEventListener('message', onMessage);
+
+ window.child = child = window.open(url, id);
+}
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/idl-helper.js b/testing/web-platform/tests/resources/test/idl-helper.js
new file mode 100644
index 0000000000..2b73527ff2
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/idl-helper.js
@@ -0,0 +1,24 @@
+"use strict";
+
+var typedefFrom = interfaceFrom;
+var dictionaryFrom = interfaceFrom;
+function interfaceFrom(i) {
+ var idl = new IdlArray();
+ idl.add_idls(i);
+ for (var prop in idl.members) {
+ return idl.members[prop];
+ }
+}
+
+function memberFrom(m) {
+ var idl = new IdlArray();
+ idl.add_idls('interface A { ' + m + '; };');
+ return idl.members["A"].members[0];
+}
+
+function typeFrom(type) {
+ var ast = WebIDL2.parse('interface Foo { ' + type + ' a(); };');
+ ast = ast[0]; // get the first fragment
+ ast = ast.members[0]; // get the first member
+ return ast.idlType; // get the type of the first field
+}
diff --git a/testing/web-platform/tests/resources/test/nested-testharness.js b/testing/web-platform/tests/resources/test/nested-testharness.js
new file mode 100644
index 0000000000..d97c1568c7
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/nested-testharness.js
@@ -0,0 +1,80 @@
+'use strict';
+
+/**
+ * Execute testharness.js and one or more scripts in an iframe. Report the
+ * results of the execution.
+ *
+ * @param {...function|...string} bodies - a function body. If specified as a
+ * function object, it will be
+ * serialized to a string using the
+ * built-in
+ * `Function.prototype.toString` prior
+ * to inclusion in the generated
+ * iframe.
+ *
+ * @returns {Promise} eventual value describing the result of the test
+ * execution; the summary object has two properties:
+ * `harness` (a string describing the harness status) and
+ * `tests` (an object whose "own" property names are the
+ * titles of the defined sub-tests and whose associated
+ * values are the subtest statuses).
+ */
+function makeTest(...bodies) {
+ const closeScript = '<' + '/script>';
+ let src = `
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Document title</title>
+<script src="/resources/testharness.js?${Math.random()}">${closeScript}
+</head>
+
+<body>
+<div id="log"></div>`;
+ bodies.forEach((body) => {
+ src += '<script>(' + body + ')();' + closeScript;
+ });
+
+ const iframe = document.createElement('iframe');
+
+ document.body.appendChild(iframe);
+ iframe.contentDocument.write(src);
+
+ return new Promise((resolve) => {
+ window.addEventListener('message', function onMessage(e) {
+ if (e.source !== iframe.contentWindow) {
+ return;
+ }
+ if (!e.data || e.data.type !=='complete') {
+ return;
+ }
+ window.removeEventListener('message', onMessage);
+ resolve(e.data);
+ });
+
+ iframe.contentDocument.close();
+ }).then(({ tests, status }) => {
+ const summary = {
+ harness: getEnumProp(status, status.status),
+ tests: {}
+ };
+
+ tests.forEach((test) => {
+ summary.tests[test.name] = getEnumProp(test, test.status);
+ });
+
+ return summary;
+ });
+}
+
+function getEnumProp(object, value) {
+ for (let property in object) {
+ if (!/^[A-Z]+$/.test(property)) {
+ continue;
+ }
+
+ if (object[property] === value) {
+ return property;
+ }
+ }
+}
diff --git a/testing/web-platform/tests/resources/test/requirements.txt b/testing/web-platform/tests/resources/test/requirements.txt
new file mode 100644
index 0000000000..95d87c3875
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/requirements.txt
@@ -0,0 +1 @@
+html5lib==1.1
diff --git a/testing/web-platform/tests/resources/test/tests/functional/abortsignal.html b/testing/web-platform/tests/resources/test/tests/functional/abortsignal.html
new file mode 100644
index 0000000000..e6080e96ec
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/abortsignal.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<title>Test#get_signal</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ "use strict";
+
+ setup(() => {
+ assert_implements_optional(window.AbortController, "No AbortController");
+ });
+
+ let signal;
+ let observed = false;
+
+ test(t => {
+ signal = t.get_signal();
+ assert_true(signal instanceof AbortSignal, "Returns an abort signal");
+ assert_false(signal.aborted, "Signal should not be aborted before test end");
+ signal.onabort = () => observed = true;
+ }, "t.signal existence");
+
+ test(t => {
+ assert_true(signal.aborted, "Signal should be aborted after test end");
+ assert_true(observed, "onabort should have been called");
+ }, "t.signal.aborted");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "message": null,
+ "name": "t.signal existence",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "t.signal.aborted",
+ "properties": {},
+ "status_string": "PASS"
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup.html
new file mode 100644
index 0000000000..468319fdbe
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+"use strict";
+
+var log_sync;
+test(function(t) {
+ log_sync = "";
+ t.add_cleanup(function() { log_sync += "1"; });
+ t.add_cleanup(function() { log_sync += "2"; });
+ t.add_cleanup(function() { log_sync += "3"; });
+ t.add_cleanup(function() { log_sync += "4"; });
+ t.add_cleanup(function() { log_sync += "5"; });
+ log_sync += "0";
+}, "probe synchronous");
+
+test(function() {
+ if (log_sync !== "012345") {
+ throw new Error("Expected: '012345'. Actual: '" + log_sync + "'.");
+ }
+}, "Cleanup methods are invoked exactly once and in the expected sequence.");
+
+var complete, log_async;
+async_test(function(t) {
+ complete = t.step_func(function() {
+ if (log_async !== "012") {
+ throw new Error("Expected: '012'. Actual: '" + log_async + "'.");
+ }
+
+ t.done();
+ });
+}, "Cleanup methods are invoked following the completion of asynchronous tests");
+
+async_test(function(t) {
+ log_async = "";
+ t.add_cleanup(function() { log_async += "1"; });
+
+ setTimeout(t.step_func(function() {
+ t.add_cleanup(function() {
+ log_async += "2";
+ complete();
+ });
+ log_async += "0";
+ t.done();
+ }), 0);
+}, "probe asynchronous");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Cleanup methods are invoked exactly once and in the expected sequence.",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Cleanup methods are invoked following the completion of asynchronous tests",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "probe asynchronous",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "probe synchronous",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async.html
new file mode 100644
index 0000000000..07ade4b93b
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup with Promise-returning functions</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+
+<script>
+"use strict";
+var completeCount = 0;
+var counts = {
+ afterTick: null,
+ afterFirst: null
+};
+
+add_result_callback(function(result_t) {
+ completeCount += 1;
+});
+
+promise_test(function(t) {
+ t.add_cleanup(function() {
+ return new Promise(function(resolve) {
+ setTimeout(function() {
+ counts.afterTick = completeCount;
+ resolve();
+ }, 0);
+ });
+ });
+ t.add_cleanup(function() {
+ return new Promise(function(resolve) {
+
+ setTimeout(function() {
+ counts.afterFirst = completeCount;
+ resolve();
+ }, 0);
+ });
+ });
+
+ return Promise.resolve();
+}, 'promise_test with asynchronous cleanup');
+
+promise_test(function() {
+ assert_equals(
+ counts.afterTick,
+ 0,
+ "test is not asynchronously considered 'complete'"
+ );
+ assert_equals(
+ counts.afterFirst,
+ 0,
+ "test is not considered 'complete' following fulfillment of first promise"
+ );
+ assert_equals(completeCount, 1);
+
+ return Promise.resolve();
+}, "synchronously-defined promise_test");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "promise_test with asynchronous cleanup",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "synchronously-defined promise_test",
+ "message": null,
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_bad_return.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_bad_return.html
new file mode 100644
index 0000000000..867bde2c39
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_bad_return.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup with non-thenable-returning function</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+
+<script>
+"use strict";
+
+promise_test(function(t) {
+ t.add_cleanup(function() {});
+ t.add_cleanup(function() {
+ return { then: 9 };
+ });
+ t.add_cleanup(function() { return Promise.resolve(); });
+
+ return Promise.resolve();
+}, "promise_test that returns a non-thenable object in one \"cleanup\" callback");
+
+promise_test(function() {}, "The test runner is in an unpredictable state ('NOT RUN')");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Test named 'promise_test that returns a non-thenable object in one \"cleanup\" callback' specified 3 'cleanup' functions, and 1 returned a non-thenable value."
+ },
+ "summarized_tests": [
+ {
+ "status_string": "NOTRUN",
+ "name": "The test runner is in an unpredictable state ('NOT RUN')",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "promise_test that returns a non-thenable object in one \"cleanup\" callback",
+ "message": null,
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_rejection.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_rejection.html
new file mode 100644
index 0000000000..e51465e7eb
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_rejection.html
@@ -0,0 +1,94 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup with Promise-returning functions (rejection handling)</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+
+<script>
+"use strict";
+var resolve, reject;
+var completeCount = 0;
+add_result_callback(function(result_t) {
+ completeCount += 1;
+});
+promise_test(function(t) {
+ t.add_cleanup(function() {
+ return new Promise(function(_, _reject) { reject = _reject; });
+ });
+ t.add_cleanup(function() {
+ return new Promise(function(_resolve) { resolve = _resolve; });
+ });
+
+ // The following cleanup function defines empty tests so that the reported
+ // data demonstrates the intended run-time behavior without relying on the
+ // test harness's handling of errors during test cleanup (which is tested
+ // elsewhere).
+ t.add_cleanup(function() {
+ if (completeCount === 0) {
+ promise_test(
+ function() {},
+ "test is not asynchronously considered 'complete' ('NOT RUN')"
+ );
+ }
+
+ reject();
+
+ setTimeout(function() {
+ if (completeCount === 0) {
+ promise_test(
+ function() {},
+ "test is not considered 'complete' following rejection of first " +
+ "promise ('NOT RUN')"
+ );
+ }
+
+ resolve();
+ }, 0);
+ });
+
+ return Promise.resolve();
+}, "promise_test with asynchronous cleanup including rejection");
+
+promise_test(function() {}, "synchronously-defined test ('NOT RUN')");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Test named 'promise_test with asynchronous cleanup including rejection' specified 3 'cleanup' functions, and 1 failed."
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "promise_test with asynchronous cleanup including rejection",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "synchronously-defined test ('NOT RUN')",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "test is not asynchronously considered 'complete' ('NOT RUN')",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "test is not considered 'complete' following rejection of first promise ('NOT RUN')",
+ "message": null,
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_rejection_after_load.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_rejection_after_load.html
new file mode 100644
index 0000000000..f9b2846100
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_rejection_after_load.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup with Promise-returning functions (rejection handling following "load" event)</title>
+</head>
+<body>
+<h1>Promise Tests</h1>
+<p>This test demonstrates the use of <tt>promise_test</tt>. Assumes ECMAScript 6
+Promise support. Some failures are expected.</p>
+<div id="log"></div>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+<script>
+promise_test(function(t) {
+ t.add_cleanup(function() {
+ return Promise.reject(new Error("foo"));
+ });
+
+ return new Promise((resolve) => {
+ document.addEventListener("DOMContentLoaded", function() {
+ setTimeout(resolve, 0)
+ });
+ });
+}, "Test with failing cleanup that completes after DOMContentLoaded event");
+
+promise_test(function(t) {
+ return Promise.resolve();
+}, "Test that should not be run due to invalid harness state ('NOT RUN')");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Test named 'Test with failing cleanup that completes after DOMContentLoaded event' specified 1 'cleanup' function, and 1 failed."
+ },
+ "summarized_tests": [
+ {
+ "status_string": "NOTRUN",
+ "name": "Test that should not be run due to invalid harness state ('NOT RUN')",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test with failing cleanup that completes after DOMContentLoaded event",
+ "message": null,
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_timeout.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_timeout.html
new file mode 100644
index 0000000000..429536ce6e
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_timeout.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup with Promise-returning functions (timeout handling)</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+"use strict";
+
+promise_test(function(t) {
+ t.add_cleanup(function() {
+ return Promise.resolve();
+ });
+
+ t.add_cleanup(function() {
+ return new Promise(function() {});
+ });
+
+ t.add_cleanup(function() {});
+
+ t.add_cleanup(function() {
+ return new Promise(function() {});
+ });
+
+ return Promise.resolve();
+}, "promise_test with asynchronous cleanup");
+
+promise_test(function() {}, "promise_test following timed out cleanup ('NOT RUN')");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Timeout while running cleanup for test named \"promise_test with asynchronous cleanup\"."
+ },
+ "summarized_tests": [
+ {
+ "status_string": "NOTRUN",
+ "name": "promise_test following timed out cleanup ('NOT RUN')",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "promise_test with asynchronous cleanup",
+ "message": null,
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_bad_return.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_bad_return.html
new file mode 100644
index 0000000000..3cfb28adf2
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_bad_return.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup with value-returning function</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+"use strict";
+
+test(function(t) {
+ t.add_cleanup(function() {});
+ t.add_cleanup(function() { return null; });
+ t.add_cleanup(function() {
+ test(
+ function() {},
+ "The test runner is in an unpredictable state #1 ('NOT RUN')"
+ );
+
+ throw new Error();
+ });
+ t.add_cleanup(function() { return 4; });
+ t.add_cleanup(function() { return { then: function() {} }; });
+ t.add_cleanup(function() {});
+}, "Test that returns a value in three \"cleanup\" functions");
+
+test(function() {}, "The test runner is in an unpredictable state #2 ('NOT RUN')");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Test named 'Test that returns a value in three \"cleanup\" functions' specified 6 'cleanup' functions, and 1 failed, and 3 returned a non-undefined value."
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Test that returns a value in three \"cleanup\" functions",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "The test runner is in an unpredictable state #1 ('NOT RUN')",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "The test runner is in an unpredictable state #2 ('NOT RUN')",
+ "message": null,
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_count.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_count.html
new file mode 100644
index 0000000000..2c9b51c6d0
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_count.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup reported count</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+
+<script>
+promise_test(function(t) {
+ t.add_cleanup(function() {});
+ t.add_cleanup(function() {});
+ t.add_cleanup(function() { throw new Error(); });
+ new EventWatcher(t, document.body, []);
+
+ return Promise.resolve();
+}, 'test with 3 user-defined cleanup functions');
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Test named 'test with 3 user-defined cleanup functions' specified 3 'cleanup' functions, and 1 failed."
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "test with 3 user-defined cleanup functions",
+ "message": null,
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_err.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_err.html
new file mode 100644
index 0000000000..60357c66ee
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_err.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup: error</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+"use strict";
+
+test(function(t) {
+ t.add_cleanup(function() {
+ throw new Error('exception in cleanup function');
+ });
+}, "Exception in cleanup function causes harness failure.");
+
+test(function() {}, "This test should not be run.");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Test named 'Exception in cleanup function causes harness failure.' specified 1 'cleanup' function, and 1 failed."
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Exception in cleanup function causes harness failure.",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "This test should not be run.",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_err_multi.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_err_multi.html
new file mode 100644
index 0000000000..80ba1b4959
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_err_multi.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup: multiple functions with one in error</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+
+<script>
+"use strict";
+
+test(function(t) {
+ t.add_cleanup(function() {
+ throw new Error("exception in cleanup function");
+ });
+
+ // The following cleanup function defines a test so that the reported
+ // data demonstrates the intended run-time behavior, i.e. that
+ // `testharness.js` invokes all cleanup functions even when one or more
+ // throw errors.
+ t.add_cleanup(function() {
+ test(function() {}, "Verification test");
+ });
+ }, "Test with multiple cleanup functions");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Test named 'Test with multiple cleanup functions' specified 2 'cleanup' functions, and 1 failed."
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Test with multiple cleanup functions",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "Verification test",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_sync_queue.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_sync_queue.html
new file mode 100644
index 0000000000..0a61503cc8
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_sync_queue.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup: queuing tests</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+
+<script>
+"use strict";
+var firstCleaned = false;
+
+promise_test(function(t) {
+ promise_test(function() {
+ assert_true(
+ firstCleaned, "should not execute until first test is complete"
+ );
+
+ return Promise.resolve();
+ }, "test defined when no tests are queued, but one test is executing");
+
+ t.add_cleanup(function() {
+ firstCleaned = true;
+ });
+
+ return Promise.resolve();
+}, "Test with a 'cleanup' function");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "message": null,
+ "name": "Test with a 'cleanup' function",
+ "status_string": "PASS",
+ "properties": {}
+ },
+ {
+ "message": null,
+ "name": "test defined when no tests are queued, but one test is executing",
+ "status_string": "PASS",
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/api-tests-1.html b/testing/web-platform/tests/resources/test/tests/functional/api-tests-1.html
new file mode 100644
index 0000000000..9de875b0f1
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/api-tests-1.html
@@ -0,0 +1,991 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Sample HTML5 API Tests</title>
+<meta name="timeout" content="6000">
+</head>
+<body onload="load_test_attr.done()">
+<h1>Sample HTML5 API Tests</h1>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ setup_run = false;
+ setup(function() {
+ setup_run = true;
+ });
+ test(function() {assert_true(setup_run)}, "Setup function ran");
+
+ // Two examples for testing events from handler and attributes
+ var load_test_event = async_test("window onload event fires when set from the handler");
+
+ function windowLoad()
+ {
+ load_test_event.done();
+ }
+ on_event(window, "load", windowLoad);
+
+ test(function() {
+ var sequence = [];
+ var outer = document.createElement("div");
+ var inner = document.createElement("div");
+ outer.appendChild(inner);
+ document.body.appendChild(outer);
+ inner.addEventListener("click", function() {
+ sequence.push("inner");
+ }, false);
+
+ on_event(outer, "click", function() {
+ sequence.push("outer");
+ });
+ inner.click();
+
+ assert_array_equals(sequence, ["inner", "outer"]);
+ }, "on_event does not use event capture");
+
+ // see the body onload below
+ var load_test_attr = async_test("body element fires the onload event set from the attribute");
+</script>
+<script>
+ function bodyElement()
+ {
+ assert_equals(document.body, document.getElementsByTagName("body")[0]);
+ }
+ test(bodyElement, "document.body should be the first body element in the document");
+
+ test(function() {
+ assert_equals(1,1);
+ assert_equals(NaN, NaN, "NaN case");
+ assert_equals(0, 0, "Zero case");
+ }, "assert_equals tests")
+
+ test(function() {
+ assert_equals(-0, 0, "Zero case");
+ }, "assert_equals tests expected to fail")
+
+ test(function() {
+ assert_not_equals({}, {}, "object case");
+ assert_not_equals(-0, 0, "Zero case");
+ }, "assert_not_equals tests")
+
+ function testAssertPass()
+ {
+ assert_true(true);
+ }
+ test(testAssertPass, "assert_true expected to pass");
+
+ function testAssertFalse()
+ {
+ assert_true(false, "false should not be true");
+ }
+ test(testAssertFalse, "assert_true expected to fail");
+
+ function basicAssertArrayEquals()
+ {
+ assert_array_equals([1, NaN], [1, NaN], "[1, NaN] is equal to [1, NaN]");
+ }
+ test(basicAssertArrayEquals, "basic assert_array_equals test");
+
+ function assertArrayEqualsUndefined()
+ {
+ assert_array_equals(undefined, [1], "undefined equals [1]?");
+ }
+ test(assertArrayEqualsUndefined, "assert_array_equals with first param undefined");
+
+ function assertArrayEqualsTrue()
+ {
+ assert_array_equals(true, [1], "true equals [1]?");
+ }
+ test(assertArrayEqualsTrue, "assert_array_equals with first param true");
+
+ function assertArrayEqualsFalse()
+ {
+ assert_array_equals(false, [1], "false equals [1]?");
+ }
+ test(assertArrayEqualsFalse, "assert_array_equals with first param false");
+
+ function assertArrayEqualsNull()
+ {
+ assert_array_equals(null, [1], "null equals [1]?");
+ }
+ test(assertArrayEqualsNull, "assert_array_equals with first param null");
+
+ function assertArrayEqualsNumeric()
+ {
+ assert_array_equals(1, [1], "1 equals [1]?");
+ }
+ test(assertArrayEqualsNumeric, "assert_array_equals with first param 1");
+
+ function basicAssertArrayApproxEquals()
+ {
+ assert_array_approx_equals([10, 11], [11, 10], 1, "[10, 11] is approximately (+/- 1) [11, 10]")
+ }
+ test(basicAssertArrayApproxEquals, "basic assert_array_approx_equals test");
+
+ function basicAssertApproxEquals()
+ {
+ assert_approx_equals(10, 11, 1, "10 is approximately (+/- 1) 11")
+ }
+ test(basicAssertApproxEquals, "basic assert_approx_equals test");
+
+ function basicAssertLessThan()
+ {
+ assert_less_than(10, 11, "10 is less than 11")
+ }
+ test(basicAssertApproxEquals, "basic assert_less_than test");
+
+ function basicAssertGreaterThan()
+ {
+ assert_greater_than(10, 11, "10 is not greater than 11");
+ }
+ test(basicAssertGreaterThan, "assert_greater_than expected to fail");
+
+ function basicAssertGreaterThanEqual()
+ {
+ assert_greater_than_equal(10, 10, "10 is greater than or equal to 10")
+ }
+ test(basicAssertGreaterThanEqual, "basic assert_greater_than_equal test");
+
+ function basicAssertLessThanEqual()
+ {
+ assert_greater_than_equal('10', 10, "'10' is not a number")
+ }
+ test(basicAssertLessThanEqual, "assert_less_than_equal expected to fail");
+
+ function testAssertInherits() {
+ var A = function(){this.a = "a"}
+ A.prototype = {b:"b"}
+ var a = new A();
+ assert_own_property(a, "a");
+ assert_not_own_property(a, "b", "unexpected property found: \"b\"");
+ assert_inherits(a, "b");
+ }
+ test(testAssertInherits, "test for assert[_not]_own_property and insert_inherits")
+
+ test(function()
+ {
+ var a = document.createElement("a")
+ var b = document.createElement("b")
+ assert_throws_dom("NOT_FOUND_ERR", function () {a.removeChild(b)});
+ }, "Test throw DOM exception")
+
+ test(function()
+ {
+ var a = document.createElement("a")
+ var b = document.createElement("b")
+ assert_throws_js(DOMException, function () {a.removeChild(b)});
+ }, "Test throw DOMException as JS exception expected to fail")
+
+ test(function()
+ {
+ assert_throws_js(SyntaxError, function () {document.querySelector("")});
+ }, "Test throw SyntaxError DOMException where JS SyntaxError expected; expected to fail")
+
+ test(function()
+ {
+ assert_throws_js(SyntaxError, function () {JSON.parse("{")});
+ }, "Test throw JS SyntaxError")
+
+ test(function()
+ {
+ assert_throws_dom("SyntaxError", function () {document.querySelector("")});
+ }, "Test throw DOM SyntaxError")
+
+ test(function()
+ {
+ var ifr = document.createElement("iframe");
+ document.body.appendChild(ifr);
+ this.add_cleanup(() => ifr.remove());
+ assert_throws_dom("SyntaxError", ifr.contentWindow.DOMException,
+ function () {ifr.contentDocument.querySelector("")});
+ }, "Test throw DOM SyntaxError from subframe");
+
+ test(function()
+ {
+ var ifr = document.createElement("iframe");
+ document.body.appendChild(ifr);
+ this.add_cleanup(() => ifr.remove());
+ assert_throws_dom("SyntaxError",
+ function () {ifr.contentDocument.querySelector("")});
+ }, "Test throw DOM SyntaxError from subframe with incorrect global expectation; expected to fail");
+
+ test(function()
+ {
+ var ifr = document.createElement("iframe");
+ document.body.appendChild(ifr);
+ this.add_cleanup(() => ifr.remove());
+ assert_throws_dom("SyntaxError", ifr.contentWindow.DOMException,
+ function () {document.querySelector("")});
+ }, "Test throw DOM SyntaxError with incorrect expectation; expected to fail");
+
+ test(function()
+ {
+ assert_throws_dom("SyntaxError", function () {JSON.parse("{")});
+ }, "Test throw JS SyntaxError where SyntaxError DOMException expected; expected to fail")
+
+ test(function()
+ {
+ var a = document.createTextNode("a")
+ var b = document.createElement("b")
+ assert_throws_dom("NOT_FOUND_ERR", function () {a.appendChild(b)});
+ }, "Test throw DOM exception expected to fail")
+
+ test(function()
+ {
+ var e = new DOMException("I am not known", "TEST_ERROR_NO_SUCH_THING");
+ assert_throws_dom(0, function() {throw e});
+ }, "Test assert_throws_dom with ambiguous DOM-exception expected to Fail");
+
+ test(function()
+ {
+ var e = {code:0, name:"TEST_ERR", TEST_ERR:0};
+ e.constructor = DOMException;
+ assert_throws_dom("TEST_ERR", function() {throw e});
+ }, "Test assert_throws_dom with non-DOM-exception expected to Fail");
+
+ test(function()
+ {
+ var e = {code: DOMException.SYNTAX_ERR, name:"SyntaxError"};
+ e.constructor = DOMException;
+ assert_throws_dom(DOMException.SYNTAX_ERR, function() {throw e});
+ }, "Test assert_throws_dom with number code value expected to Pass");
+
+ test(function()
+ {
+ var e = new DOMException("Some message", "SyntaxError");
+ assert_throws_dom(DOMException.SYNTAX_ERR, function() {throw e});
+ }, "Test assert_throws_dom with number code value and real DOMException expected to Pass");
+
+ var t = async_test("Test step_func")
+ setTimeout(
+ t.step_func(
+ function () {
+ assert_true(true); t.done();
+ }), 0);
+
+ async_test(function(t) {
+ setTimeout(t.step_func(function (){assert_true(true); t.done();}), 0);
+ }, "Test async test with callback");
+
+ async_test(function() {
+ setTimeout(this.step_func(function (){assert_true(true); this.done();}), 0);
+ }, "Test async test with callback and `this` obj.");
+
+ async_test("test should timeout (fail) with the default of 2 seconds").step(function(){});
+
+ async_test("async test that is never started, should have status Not Run");
+
+
+ test(function(t) {
+ window.global = 1;
+ t.add_cleanup(function() {delete window.global});
+ assert_equals(window.global, 1);
+ },
+ "Test that defines a global and cleans it up");
+
+ test(function() {assert_equals(window.global, undefined)},
+ "Test that cleanup handlers from previous test ran");
+
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "TIMEOUT",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Setup function ran",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Test assert_throws_dom with ambiguous DOM-exception expected to Fail",
+ "message": "Test bug: ambiguous DOMException code 0 passed to assert_throws_dom()",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Test assert_throws_dom with non-DOM-exception expected to Fail",
+ "message": "Test bug: unrecognized DOMException code name or name \"TEST_ERR\" passed to assert_throws_dom()",
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test assert_throws_dom with number code value expected to Pass",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test assert_throws_dom with number code value and real DOMException expected to Pass",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test async test with callback",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test async test with callback and `this` obj.",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test step_func",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test that cleanup handlers from previous test ran",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test that defines a global and cleans it up",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test throw DOM exception",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Test throw DOMException as JS exception expected to fail",
+ "message": "assert_throws_js: function \"function DOMException() {\n [native code]\n}\" is not an Error subtype",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Test throw SyntaxError DOMException where JS SyntaxError expected; expected to fail",
+ "message": "assert_throws_js: function \"function () {document.querySelector(\"\")}\" threw object \"SyntaxError: Document.querySelector: '' is not a valid selector\" (\"SyntaxError\") expected instance of function \"function SyntaxError() {\n [native code]\n}\" (\"SyntaxError\")",
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test throw JS SyntaxError",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test throw DOM SyntaxError",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test throw DOM SyntaxError from subframe",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Test throw DOM SyntaxError from subframe with incorrect global expectation; expected to fail",
+ "message": "assert_throws_dom: function \"function () {ifr.contentDocument.querySelector(\"\")}\" threw an exception from the wrong global",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Test throw DOM SyntaxError with incorrect expectation; expected to fail",
+ "message": "assert_throws_dom: function \"function () {document.querySelector(\"\")}\" threw an exception from the wrong global",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Test throw JS SyntaxError where SyntaxError DOMException expected; expected to fail",
+ "message": "assert_throws_dom: function \"function () {JSON.parse(\"{\")}\" threw object \"SyntaxError: JSON.parse: end of data while reading object contents at line 1 column 2 of the JSON data\" that is not a DOMException SyntaxError: property \"code\" is equal to undefined, expected 12",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Test throw DOM exception expected to fail",
+ "message": "assert_throws_dom: function \"function () {a.appendChild(b)}\" threw object \"HierarchyRequestError: Node.appendChild: Cannot add children to a Text\" that is not a DOMException NOT_FOUND_ERR: property \"code\" is equal to 3, expected 8",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "assert_array_equals with first param 1",
+ "message": "assert_array_equals: 1 equals [1]? value is 1, expected array",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "assert_array_equals with first param false",
+ "message": "assert_array_equals: false equals [1]? value is false, expected array",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "assert_array_equals with first param null",
+ "message": "assert_array_equals: null equals [1]? value is null, expected array",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "assert_array_equals with first param true",
+ "message": "assert_array_equals: true equals [1]? value is true, expected array",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "assert_array_equals with first param undefined",
+ "message": "assert_array_equals: undefined equals [1]? value is undefined, expected array",
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_equals tests",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "assert_equals tests expected to fail",
+ "message": "assert_equals: Zero case expected 0 but got -0",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "assert_greater_than expected to fail",
+ "message": "assert_greater_than: 10 is not greater than 11 expected a number greater than 11 but got 10",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "assert_less_than_equal expected to fail",
+ "message": "assert_greater_than_equal: '10' is not a number expected a number but got a \"string\"",
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_not_equals tests",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "assert_true expected to fail",
+ "message": "assert_true: false should not be true expected true got false",
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_true expected to pass",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "async test that is never started, should have status Not Run",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "basic assert_approx_equals test",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "basic assert_array_approx_equals test",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "basic assert_array_equals test",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "basic assert_greater_than_equal test",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "basic assert_less_than test",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "body element fires the onload event set from the attribute",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "document.body should be the first body element in the document",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "test for assert[_not]_own_property and insert_inherits",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "TIMEOUT",
+ "name": "test should timeout (fail) with the default of 2 seconds",
+ "message": "Test timed out",
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "window onload event fires when set from the handler",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "on_event does not use event capture",
+ "message": null,
+ "properties": {}
+ }
+ ],
+ "summarized_asserts": [
+ {
+ "assert_name": "assert_true",
+ "test": "Setup function ran",
+ "args": [
+ "true"
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_array_equals",
+ "test": "on_event does not use event capture",
+ "args": [
+ "[\"inner\", \"outer\"]",
+ "[\"inner\", \"outer\"]"
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_equals",
+ "test": "document.body should be the first body element in the document",
+ "args": [
+ "Element node <body onload=\"load_test_attr.done()\"> <h1>Sample HTML5 AP...",
+ "Element node <body onload=\"load_test_attr.done()\"> <h1>Sample HTML5 AP..."
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_equals",
+ "test": "assert_equals tests",
+ "args": [
+ "1",
+ "1"
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_equals",
+ "test": "assert_equals tests",
+ "args": [
+ "NaN",
+ "NaN",
+ "\"NaN case\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_equals",
+ "test": "assert_equals tests",
+ "args": [
+ "0",
+ "0",
+ "\"Zero case\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_equals",
+ "test": "assert_equals tests expected to fail",
+ "args": [
+ "-0",
+ "0",
+ "\"Zero case\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_not_equals",
+ "test": "assert_not_equals tests",
+ "args": [
+ "object \"[object Object]\"",
+ "object \"[object Object]\"",
+ "\"object case\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_not_equals",
+ "test": "assert_not_equals tests",
+ "args": [
+ "-0",
+ "0",
+ "\"Zero case\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_true",
+ "test": "assert_true expected to pass",
+ "args": [
+ "true"
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_true",
+ "test": "assert_true expected to fail",
+ "args": [
+ "false",
+ "\"false should not be true\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_array_equals",
+ "test": "basic assert_array_equals test",
+ "args": [
+ "[1, NaN]",
+ "[1, NaN]",
+ "\"[1, NaN] is equal to [1, NaN]\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_array_equals",
+ "test": "assert_array_equals with first param undefined",
+ "args": [
+ "undefined",
+ "[1]",
+ "\"undefined equals [1]?\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_array_equals",
+ "test": "assert_array_equals with first param true",
+ "args": [
+ "true",
+ "[1]",
+ "\"true equals [1]?\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_array_equals",
+ "test": "assert_array_equals with first param false",
+ "args": [
+ "false",
+ "[1]",
+ "\"false equals [1]?\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_array_equals",
+ "test": "assert_array_equals with first param null",
+ "args": [
+ "null",
+ "[1]",
+ "\"null equals [1]?\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_array_equals",
+ "test": "assert_array_equals with first param 1",
+ "args": [
+ "1",
+ "[1]",
+ "\"1 equals [1]?\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_array_approx_equals",
+ "test": "basic assert_array_approx_equals test",
+ "args": [
+ "[10, 11]",
+ "[11, 10]",
+ "1",
+ "\"[10, 11] is approximately (+/- 1) [11, 10]\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_approx_equals",
+ "test": "basic assert_approx_equals test",
+ "args": [
+ "10",
+ "11",
+ "1",
+ "\"10 is approximately (+/- 1) 11\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_approx_equals",
+ "test": "basic assert_less_than test",
+ "args": [
+ "10",
+ "11",
+ "1",
+ "\"10 is approximately (+/- 1) 11\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_greater_than",
+ "test": "assert_greater_than expected to fail",
+ "args": [
+ "10",
+ "11",
+ "\"10 is not greater than 11\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_greater_than_equal",
+ "test": "basic assert_greater_than_equal test",
+ "args": [
+ "10",
+ "10",
+ "\"10 is greater than or equal to 10\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_greater_than_equal",
+ "test": "assert_less_than_equal expected to fail",
+ "args": [
+ "\"10\"",
+ "10",
+ "\"'10' is not a number\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_own_property",
+ "test": "test for assert[_not]_own_property and insert_inherits",
+ "args": [
+ "object \"[object Object]\"",
+ "\"a\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_not_own_property",
+ "test": "test for assert[_not]_own_property and insert_inherits",
+ "args": [
+ "object \"[object Object]\"",
+ "\"b\"",
+ "\"unexpected property found: \\\"b\\\"\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_inherits",
+ "test": "test for assert[_not]_own_property and insert_inherits",
+ "args": [
+ "object \"[object Object]\"",
+ "\"b\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test throw DOM exception",
+ "args": [
+ "\"NOT_FOUND_ERR\"",
+ "function \"function () {a.removeChild(b)}\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_throws_js",
+ "test": "Test throw DOMException as JS exception expected to fail",
+ "args": [
+ "function \"function DOMException() { [native code] }\"",
+ "function \"function () {a.removeChild(b)}\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_throws_js",
+ "test": "Test throw SyntaxError DOMException where JS SyntaxError expected; expected to fail",
+ "args": [
+ "function \"function SyntaxError() { [native code] }\"",
+ "function \"function () {document.querySelector(\"\")}\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_throws_js",
+ "test": "Test throw JS SyntaxError",
+ "args": [
+ "function \"function SyntaxError() { [native code] }\"",
+ "function \"function () {JSON.parse(\"{\")}\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test throw DOM SyntaxError",
+ "args": [
+ "\"SyntaxError\"",
+ "function \"function () {document.querySelector(\"\")}\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test throw DOM SyntaxError from subframe",
+ "args": [
+ "\"SyntaxError\"",
+ "function \"function DOMException() { [native code] }\"",
+ "function \"function () {ifr.contentDocument.querySelector(\"\")}\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test throw DOM SyntaxError from subframe with incorrect global expectation; expected to fail",
+ "args": [
+ "\"SyntaxError\"",
+ "function \"function () {ifr.contentDocument.querySelector(\"\")}\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test throw DOM SyntaxError with incorrect expectation; expected to fail",
+ "args": [
+ "\"SyntaxError\"",
+ "function \"function DOMException() { [native code] }\"",
+ "function \"function () {document.querySelector(\"\")}\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test throw JS SyntaxError where SyntaxError DOMException expected; expected to fail",
+ "args": [
+ "\"SyntaxError\"",
+ "function \"function () {JSON.parse(\"{\")}\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test throw DOM exception expected to fail",
+ "args": [
+ "\"NOT_FOUND_ERR\"",
+ "function \"function () {a.appendChild(b)}\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test assert_throws_dom with ambiguous DOM-exception expected to Fail",
+ "args": [
+ "0",
+ "function \"function() {throw e}\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test assert_throws_dom with non-DOM-exception expected to Fail",
+ "args": [
+ "\"TEST_ERR\"",
+ "function \"function() {throw e}\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test assert_throws_dom with number code value expected to Pass",
+ "args": [
+ "12",
+ "function \"function() {throw e}\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test assert_throws_dom with number code value and real DOMException expected to Pass",
+ "args": [
+ "12",
+ "function \"function() {throw e}\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_equals",
+ "test": "Test that defines a global and cleans it up",
+ "args": [
+ "1",
+ "1"
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_equals",
+ "test": "Test that cleanup handlers from previous test ran",
+ "args": [
+ "undefined",
+ "undefined"
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_true",
+ "test": "Test step_func",
+ "args": [
+ "true"
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_true",
+ "test": "Test async test with callback",
+ "args": [
+ "true"
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_true",
+ "test": "Test async test with callback and `this` obj.",
+ "args": [
+ "true"
+ ],
+ "status": 0
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/api-tests-2.html b/testing/web-platform/tests/resources/test/tests/functional/api-tests-2.html
new file mode 100644
index 0000000000..9af94f61ac
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/api-tests-2.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Sample HTML5 API Tests</title>
+</head>
+<body>
+<h1>Sample HTML5 API Tests</h1>
+<p>There should be two results</p>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+setup({explicit_done:true})
+test(function() {assert_true(true)}, "Test defined before onload");
+
+onload = function() {test(function (){assert_true(true)}, "Test defined after onload");
+done();
+}
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Test defined after onload",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test defined before onload",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "summarized_asserts": [
+ {
+ "assert_name": "assert_true",
+ "test": "Test defined before onload",
+ "args": [
+ "true"
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_true",
+ "test": "Test defined after onload",
+ "args": [
+ "true"
+ ],
+ "status": 0
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/api-tests-3.html b/testing/web-platform/tests/resources/test/tests/functional/api-tests-3.html
new file mode 100644
index 0000000000..991fc6da67
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/api-tests-3.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Sample HTML5 API Tests</title>
+</head>
+<script src="/resources/testharness.js"></script>
+
+<body>
+<h1>Sample HTML5 API Tests</h1>
+<div id="log"></div>
+<script>
+setup({explicit_timeout:true});
+var t = async_test("This test should give a status of 'Not Run' without a delay");
+timeout();
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "TIMEOUT",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "NOTRUN",
+ "name": "This test should give a status of 'Not Run' without a delay",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/assert-array-equals.html b/testing/web-platform/tests/resources/test/tests/functional/assert-array-equals.html
new file mode 100644
index 0000000000..b6460a4868
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/assert-array-equals.html
@@ -0,0 +1,162 @@
+<!DOCTYPE HTML>
+<title>assert_array_equals</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+test(() => {
+ assert_array_equals([], []);
+}, "empty and equal");
+test(() => {
+ assert_array_equals([1], [1]);
+}, "non-empty and equal");
+test(() => {
+ assert_array_equals([], [1]);
+}, "length differs");
+test(() => {
+ assert_array_equals([1], [,]);
+}, "property is present");
+test(() => {
+ assert_array_equals([,], [1]);
+}, "property is missing");
+test(() => {
+ assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20], ["x",1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]);
+}, "property 0 differs");
+test(() => {
+ assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20], [0,1,2,3,4,"x",6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]);
+}, "property 5 differs");
+test(() => {
+ assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21], [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]);
+}, "lengths differ and input array beyond display limit");
+test(() => {
+ assert_array_equals([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]);
+}, "lengths differ and expected array beyond display limit");
+test(() => {
+ assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21], [,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]);
+}, "property 0 is present and arrays are beyond display limit");
+test(() => {
+ assert_array_equals([,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]);
+}, "property 0 is missing and arrays are beyond display limit");
+test(() => {
+ assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,,19,20,21]);
+}, "property 18 is present and arrays are beyond display limit");
+test(() => {
+ assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,,19,20,21], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]);
+}, "property 18 is missing and arrays are beyond display limit");
+test(() => {
+ assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21], ["x",1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]);
+}, "property 0 differs and arrays are beyond display limit");
+test(() => {
+ assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21], [0,1,2,3,4,"x",6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]);
+}, "property 5 differs and arrays are beyond display limit");
+test(() => {
+ assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26], [0,1,2,3,4,5,6,7,8,9,10,11,"x",13,14,15,16,17,18,19,20,21,22,23,24,25,26]);
+}, "property 5 differs and arrays are beyond display limit on both sides");
+</script>
+<script type="text/json" id="expected">
+{
+ "type": "complete",
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "name": "empty and equal",
+ "message": null,
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "name": "length differs",
+ "message": "assert_array_equals: lengths differ, expected array [1] length 1, got [] length 0",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "lengths differ and expected array beyond display limit",
+ "message": "assert_array_equals: lengths differ, expected array [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21] length 22, got [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21] length 21",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "lengths differ and input array beyond display limit",
+ "message": "assert_array_equals: lengths differ, expected array [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21] length 21, got [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21] length 22",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "non-empty and equal",
+ "message": null,
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "name": "property 0 differs",
+ "message": "assert_array_equals: expected property 0 to be \"x\" but got 0 (expected array [\"x\", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] got [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property 0 differs and arrays are beyond display limit",
+ "message": "assert_array_equals: expected property 0 to be \"x\" but got 0 (expected array [\"x\", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026] got [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property 0 is missing and arrays are beyond display limit",
+ "message": "assert_array_equals: expected property 0 to be \"present\" but was \"missing\" (expected array [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026] got [, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property 0 is present and arrays are beyond display limit",
+ "message": "assert_array_equals: expected property 0 to be \"missing\" but was \"present\" (expected array [, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026] got [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property 18 is missing and arrays are beyond display limit",
+ "message": "assert_array_equals: expected property 18 to be \"present\" but was \"missing\" (expected array [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21] got [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, , 19, 20, 21])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property 18 is present and arrays are beyond display limit",
+ "message": "assert_array_equals: expected property 18 to be \"missing\" but was \"present\" (expected array [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, , 19, 20, 21] got [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property 5 differs",
+ "message": "assert_array_equals: expected property 5 to be \"x\" but got 5 (expected array [0, 1, 2, 3, 4, \"x\", 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] got [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property 5 differs and arrays are beyond display limit",
+ "message": "assert_array_equals: expected property 5 to be \"x\" but got 5 (expected array [0, 1, 2, 3, 4, \"x\", 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026] got [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property 5 differs and arrays are beyond display limit on both sides",
+ "message": "assert_array_equals: expected property 12 to be \"x\" but got 12 (expected array [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, \"x\", 13, 14, 15, 16, 17, 18, 19, 20, 21, \u2026] got [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, \u2026])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property is missing",
+ "message": "assert_array_equals: expected property 0 to be \"present\" but was \"missing\" (expected array [1] got [])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property is present",
+ "message": "assert_array_equals: expected property 0 to be \"missing\" but was \"present\" (expected array [] got [1])",
+ "properties": {},
+ "status_string": "FAIL"
+ }
+ ]
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/assert-throws-dom.html b/testing/web-platform/tests/resources/test/tests/functional/assert-throws-dom.html
new file mode 100644
index 0000000000..4dd66b2372
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/assert-throws-dom.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<title>assert_throws_dom</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+test(() => {
+ function f() {
+ assert_true(false, "Trivial assertion.");
+
+ // Would lead to throwing a SyntaxError.
+ document.createElement("div").contentEditable = "invalid";
+ }
+
+ assert_throws_dom("SyntaxError", () => { f(); });
+}, "Violated assertion nested in `assert_throws_dom`.");
+</script>
+<script type="text/json" id="expected">
+{
+ "type": "complete",
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "message": "assert_true: Trivial assertion. expected true got false",
+ "name": "Violated assertion nested in `assert_throws_dom`.",
+ "properties": {},
+ "status_string": "FAIL"
+ }
+ ],
+ "summarized_asserts": [
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Violated assertion nested in `assert_throws_dom`.",
+ "args": [
+ "\"SyntaxError\"",
+ "function \"() => { f(); }\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_true",
+ "test": "Violated assertion nested in `assert_throws_dom`.",
+ "args": [
+ "false",
+ "\"Trivial assertion.\""
+ ],
+ "status": 1
+ }
+ ]
+}
+</script>
+
diff --git a/testing/web-platform/tests/resources/test/tests/functional/force_timeout.html b/testing/web-platform/tests/resources/test/tests/functional/force_timeout.html
new file mode 100644
index 0000000000..2058fdb862
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/force_timeout.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#force_timeout</title>
+</head>
+<body>
+<h1>Test#force_timeout</h1>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+setup({ explicit_timeout: true });
+
+test(function(t) {
+ t.force_timeout();
+ }, 'test (synchronous)');
+
+async_test(function(t) {
+ t.step_timeout(function() {
+ t.force_timeout();
+ }, 0);
+ }, 'async_test');
+
+promise_test(function(t) {
+ t.force_timeout();
+
+ return new Promise(function() {});
+ }, 'promise_test');
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "TIMEOUT",
+ "name": "async_test",
+ "message": "Test timed out",
+ "properties": {}
+ },
+ {
+ "status_string": "TIMEOUT",
+ "name": "promise_test",
+ "message": "Test timed out",
+ "properties": {}
+ },
+ {
+ "status_string": "TIMEOUT",
+ "name": "test (synchronous)",
+ "message": "Test timed out",
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/generate-callback.html b/testing/web-platform/tests/resources/test/tests/functional/generate-callback.html
new file mode 100644
index 0000000000..11d41743b3
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/generate-callback.html
@@ -0,0 +1,153 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Sample for using generate_tests to create a series of tests that share the same callback.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<script>
+// generate_tests takes an array of arrays that define tests
+// but lets pass it an empty array and verify it does nothing.
+function null_callback() {
+ throw "null_callback should not be called.";
+}
+generate_tests(null_callback, []);
+
+// Generate 3 tests specifying the name and one parameter
+function validate_arguments(arg1) {
+ assert_equals(arg1, 1, "Ensure that we get our expected argument");
+}
+generate_tests(validate_arguments, [
+ ["first test", 1],
+ ["second test", 1],
+ ["third test", 1],
+]);
+
+// Generate a test passing in a properties object that is shared across tests.
+function validate_properties() {
+ assert_true(this.properties.sentinel, "Ensure that we got the right properties object.");
+}
+generate_tests(validate_properties, [["sentinel check 1"], ["sentinel check 2"]], {sentinel: true});
+
+// Generate a test passing in a properties object that is shared across tests.
+function validate_separate_properties() {
+ if (this.name === "sentinel check 1 unique properties") {
+ assert_true(this.properties.sentinel, "Ensure that we got the right properties object. Expect sentinel: true.");
+ }
+ else {
+ assert_false(this.properties.sentinel, "Ensure that we got the right properties object. Expect sentinel: false.");
+ }
+}
+generate_tests(validate_separate_properties, [["sentinel check 1 unique properties"], ["sentinel check 2 unique properties"]], [{sentinel: true}, {sentinel: false}]);
+
+// Finally generate a complicated set of tests from another data source
+var letters = ["a", "b", "c", "d", "e", "f"];
+var numbers = [0, 1, 2, 3, 4, 5];
+function validate_related_arguments(arg1, arg2) {
+ assert_equals(arg1.charCodeAt(0) - "a".charCodeAt(0), arg2, "Ensure that we can map letters to numbers.");
+}
+function format_as_test(letter, index, letters) {
+ return ["Test to map " + letter + " to " + numbers[index], letter, numbers[index]];
+}
+generate_tests(validate_related_arguments, letters.map(format_as_test));
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Test to map a to 0",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test to map b to 1",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test to map c to 2",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test to map d to 3",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test to map e to 4",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test to map f to 5",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "first test",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "second test",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "sentinel check 1",
+ "properties": {
+ "sentinel": true
+ },
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "sentinel check 1 unique properties",
+ "properties": {
+ "sentinel": true
+ },
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "sentinel check 2",
+ "properties": {
+ "sentinel": true
+ },
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "sentinel check 2 unique properties",
+ "properties": {
+ "sentinel": false
+ },
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "third test",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlDictionary/test_partial_interface_of.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlDictionary/test_partial_interface_of.html
new file mode 100644
index 0000000000..05e6e0b1e0
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlDictionary/test_partial_interface_of.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <meta name="variant" content="">
+ <title>idlharness: Partial dictionary</title>
+ <script src="/resources/test/variants.js"></script>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+</head>
+
+<body>
+ <p>Verify the series of sub-tests that are executed for "partial" dictionary objects.</p>
+ <script>
+ "use strict";
+
+ // No original existence
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls('partial dictionary A {};');
+ idlArray.test();
+ })();
+
+ // Multiple partials existence
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_untested_idls('partial dictionary B {};');
+ idlArray.add_idls('partial dictionary B {};');
+ idlArray.add_idls('partial dictionary B {};');
+ idlArray.add_idls('partial dictionary B {};');
+ idlArray.add_idls('dictionary B {};');
+ idlArray.test();
+ })();
+
+ // Original is a namespace, not a dictionary.
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls(`
+ partial dictionary C {};
+ namespace C {};`);
+ idlArray.merge_partials();
+ })();
+ </script>
+ <script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "name": "Partial dictionary A: original dictionary defined",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: Original dictionary should be defined expected true got false"
+ },
+ {
+ "name": "Partial dictionary B[2]: original dictionary defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial dictionary B[3]: original dictionary defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial dictionary B[4]: original dictionary defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial dictionary C: original dictionary defined",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: Original C definition should have type dictionary expected true got false"
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_exposed_wildcard.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_exposed_wildcard.html
new file mode 100644
index 0000000000..addc0eb4fc
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_exposed_wildcard.html
@@ -0,0 +1,233 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>idlharness: Exposed=*</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+</head>
+<body>
+<script>
+"use strict";
+
+Object.defineProperty(window, "A", {
+ enumerable: false,
+ writable: true,
+ configurable: true,
+ value: function A() {}
+ });
+Object.defineProperty(window.A, "prototype", {
+ writable: false,
+ value: window.A.prototype
+ });
+A.prototype[Symbol.toStringTag] = "A";
+
+Object.defineProperty(window, "C", {
+ enumerable: false,
+ writable: true,
+ configurable: true,
+ value: function C() {}
+ });
+Object.defineProperty(window.C, "prototype", {
+ writable: false,
+ value: window.C.prototype
+ });
+C.prototype[Symbol.toStringTag] = "C";
+
+Object.defineProperty(window, "D", {
+ enumerable: false,
+ writable: true,
+ configurable: true,
+ value: function D() {}
+ });
+Object.defineProperty(window.D, "prototype", {
+ writable: false,
+ value: window.D.prototype
+ });
+C.prototype[Symbol.toStringTag] = "D";
+Object.defineProperty(window, "B", {
+ enumerable: false,
+ writable: true,
+ configurable: true,
+ value: window.A
+ });
+
+var idlArray = new IdlArray();
+idlArray.add_idls(`
+[Exposed=*, LegacyWindowAlias=B] interface A {};
+[Exposed=*] partial interface A {};
+[Exposed=Window] interface C {};
+[Exposed=*] partial interface C {};
+[Exposed=*] interface D {};
+[Exposed=Window] partial interface D {};
+`);
+idlArray.add_objects({
+ Window: ["window"]
+});
+idlArray.test();
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "name": "A interface object length",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "A interface object name",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "A interface: existence and properties of interface object",
+ "properties": {},
+ "message": "assert_throws_js: interface object didn't throw TypeError when called as a function function \"function() {\n interface_object();\n }\" did not throw",
+ "status_string": "FAIL"
+ },
+ {
+ "name": "A interface: existence and properties of interface prototype object",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "A interface: existence and properties of interface prototype object's \"constructor\" property",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "A interface: existence and properties of interface prototype object's @@unscopables property",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "A interface: legacy window alias",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "C interface object length",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "C interface object name",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "C interface: existence and properties of interface object",
+ "properties": {},
+ "message": "assert_throws_js: interface object didn't throw TypeError when called as a function function \"function() {\n interface_object();\n }\" did not throw",
+ "status_string": "FAIL"
+ },
+ {
+ "name": "C interface: existence and properties of interface prototype object",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "C interface: existence and properties of interface prototype object's \"constructor\" property",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "C interface: existence and properties of interface prototype object's @@unscopables property",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "D interface object length",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "D interface object name",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "D interface: existence and properties of interface object",
+ "properties": {},
+ "message": "assert_throws_js: interface object didn't throw TypeError when called as a function function \"function() {\n interface_object();\n }\" did not throw",
+ "status_string": "FAIL"
+ },
+ {
+ "name": "D interface: existence and properties of interface prototype object",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "D interface: existence and properties of interface prototype object's \"constructor\" property",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "D interface: existence and properties of interface prototype object's @@unscopables property",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "Partial interface A: original interface defined",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "Partial interface A: valid exposure set",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "Partial interface C: original interface defined",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "Partial interface C: valid exposure set",
+ "properties": {},
+ "message": "Partial C interface is exposed everywhere, the original interface is not.",
+ "status_string": "FAIL"
+ },
+ {
+ "name": "Partial interface D: original interface defined",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "Partial interface D: valid exposure set",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_immutable_prototype.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_immutable_prototype.html
new file mode 100644
index 0000000000..5fe05915b0
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_immutable_prototype.html
@@ -0,0 +1,298 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>idlharness: Immutable prototypes</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+</head>
+<body>
+<script>
+"use strict";
+
+Object.defineProperty(window, "Foo", {
+ enumerable: false,
+ writable: true,
+ configurable: true,
+ value: function Foo() {
+ if (!new.target) {
+ throw new TypeError('Foo() must be called with new');
+ }
+ }
+ });
+Object.defineProperty(window.Foo, "prototype", {
+ writable: false,
+ value: window.Foo.prototype
+ });
+Foo.prototype[Symbol.toStringTag] = "Foo";
+
+var idlArray = new IdlArray();
+idlArray.add_untested_idls("interface EventTarget {};");
+idlArray.add_idls(
+ "[Global=Window, Exposed=Window]\n" +
+ "interface Window : EventTarget {};\n" +
+
+ "[Global=Window, Exposed=Window]\n" +
+ "interface Foo { constructor(); };"
+ );
+idlArray.add_objects({
+ Foo: ["new Foo()"],
+ Window: ["window"]
+});
+idlArray.test();
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "name": "Foo interface object length",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface object name",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: existence and properties of interface object",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: existence and properties of interface prototype object",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: existence and properties of interface prototype object's \"constructor\" property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: existence and properties of interface prototype object's @@unscopables property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via Object.setPrototypeOf should throw a TypeError",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_throws_js: function \"function() {\n Object.setPrototypeOf(obj, newValue);\n }\" did not throw"
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via Reflect.setPrototypeOf should return false",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_false: expected false got true"
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via __proto__ should throw a TypeError",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_throws_js: function \"function() {\n obj.__proto__ = newValue;\n }\" did not throw"
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via Object.setPrototypeOf should not throw",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via Reflect.setPrototypeOf should return true",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via __proto__ should not throw",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via Object.setPrototypeOf should throw a TypeError",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_throws_js: function \"function() {\n Object.setPrototypeOf(obj, newValue);\n }\" did not throw"
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via Reflect.setPrototypeOf should return false",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_false: expected false got true"
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via __proto__ should throw a TypeError",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_throws_js: function \"function() {\n obj.__proto__ = newValue;\n }\" did not throw"
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via Object.setPrototypeOf should not throw",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via Reflect.setPrototypeOf should return true",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via __proto__ should not throw",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo must be primary interface of new Foo()",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Stringification of new Foo()",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Stringification of window",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface object length",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface object name",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: existence and properties of interface object",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: existence and properties of interface prototype object",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: existence and properties of interface prototype object's \"constructor\" property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: existence and properties of interface prototype object's @@unscopables property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via Object.setPrototypeOf should throw a TypeError",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via Reflect.setPrototypeOf should return false",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via __proto__ should throw a TypeError",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via Object.setPrototypeOf should not throw",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via Reflect.setPrototypeOf should return true",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via __proto__ should not throw",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via Object.setPrototypeOf should throw a TypeError",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via Reflect.setPrototypeOf should return false",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via __proto__ should throw a TypeError",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via Object.setPrototypeOf should not throw",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via Reflect.setPrototypeOf should return true",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via __proto__ should not throw",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window must be primary interface of window",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_interface_mixin.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_interface_mixin.html
new file mode 100644
index 0000000000..be2844e698
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_interface_mixin.html
@@ -0,0 +1,131 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>idlharness: interface mixins</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+</head>
+
+<body>
+ <p>Verify the series of sub-tests that are executed for "interface mixin" objects.</p>
+ <script>
+ "use strict";
+
+ // Simple includes statement (valid)
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls(`
+ [Exposed=Window] interface I1 {};
+ interface mixin M1 { attribute any a1; };
+ I1 includes M1;`);
+ idlArray.merge_partials();
+ idlArray.merge_mixins();
+ })();
+
+ // Partial interface mixin (valid)
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls(`
+ [Exposed=Window] interface I2 {};
+ interface mixin M2 {};
+ partial interface mixin M2 { attribute any a2; };
+ I2 includes M2;`);
+ idlArray.merge_partials();
+ idlArray.merge_mixins();
+ })();
+
+ // Partial interface mixin without original mixin
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls('partial interface mixin M3 {};');
+ idlArray.merge_partials();
+ idlArray.merge_mixins();
+ })();
+
+ // Name clash between mixin and partial mixin
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls(`
+ interface mixin M4 { attribute any a4; };
+ partial interface mixin M4 { attribute any a4; };`);
+ idlArray.merge_partials();
+ idlArray.merge_mixins();
+ })();
+
+ // Name clash between interface and mixin
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_untested_idls(`
+ interface mixin M5 { attribute any a5; };
+ interface I5 { attribute any a5; };
+ I5 includes M5;`);
+ idlArray.merge_partials();
+ idlArray.merge_mixins();
+ })();
+ </script>
+ <script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "name": "I1 includes M1: member names are unique",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "I2 includes M2: member names are unique",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "I5 includes M5: member names are unique",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: member a5 is unique expected true got false"
+ },
+ {
+ "name": "Partial interface mixin M2: member names are unique",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial interface mixin M2: original interface mixin defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial interface mixin M3: original interface mixin defined",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: Original interface mixin should be defined expected true got false"
+ },
+ {
+ "name": "Partial interface mixin M4: member names are unique",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: member a4 is unique expected true got false"
+ },
+ {
+ "name": "Partial interface mixin M4: original interface mixin defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_partial_interface_of.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_partial_interface_of.html
new file mode 100644
index 0000000000..671196cc5d
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_partial_interface_of.html
@@ -0,0 +1,188 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <meta name="variant" content="">
+ <title>idlharness: Partial interface</title>
+ <script src="/resources/test/variants.js"></script>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+</head>
+
+<body>
+ <p>Verify the series of sub-tests that are executed for "partial" interface objects.</p>
+ <script>
+ "use strict";
+
+ // No original existence
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls('partial interface A {};');
+ idlArray.test();
+ })();
+
+ // Valid exposure (Note: Worker -> {Shared,Dedicated,Service}Worker)
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_untested_idls(`
+ [Exposed=(Worker)]
+ interface B {};
+
+ [Exposed=(DedicatedWorker, ServiceWorker, SharedWorker)]
+ interface C {};`);
+ idlArray.add_idls(`
+ [Exposed=(DedicatedWorker, ServiceWorker, SharedWorker)]
+ partial interface B {};
+
+ [Exposed=(Worker)]
+ partial interface C {};`);
+ idlArray.merge_partials();
+ })();
+
+ // Invalid exposure
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_untested_idls(`
+ [Exposed=(Window, ServiceWorker)]
+ interface D {};`);
+ idlArray.add_idls(`
+ [Exposed=(DedicatedWorker)]
+ partial interface D {};`);
+ idlArray.merge_partials();
+ })();
+
+ // Original is a namespace, not an interface.
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls(`
+ partial interface E {};
+ namespace E {};`);
+ idlArray.merge_partials();
+ })();
+
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls(`
+ partial interface F {};
+ partial interface mixin G {};
+ `);
+ idlArray.add_dependency_idls(`
+ interface F {};
+ interface mixin G {};
+ interface mixin H {};
+ F includes H;
+ I includes H;
+ J includes G;
+ interface K : J {};
+ interface L : F {};
+ `);
+ test(() => {
+ // Convert idlArray.includes into a Map from name of target interface to
+ // name of included mixin. (This assumes each interface includes at most
+ // one mixin, otherwise later includes would clobber earlier ones.)
+ const includes = new Map(idlArray.includes.map(i => [i.target, i.includes]));
+ // F is tested, so H is a dep.
+ assert_equals(includes.get('F'), 'H', 'F should be picked up');
+ // H is not tested, so I is not a dep.
+ assert_false(includes.has('I'), 'I should be ignored');
+ // G is a dep, so J is a dep.
+ assert_equals(includes.get('J'), 'G', 'J should be picked up');
+ // K isn't a dep because J isn't defined.
+ assert_false('K' in idlArray.members, 'K should be ignored');
+ // L isn't a dep because F is untested.
+ assert_false('L' in idlArray.members, 'L should be ignored');
+ }, 'partial mixin dep implications');
+ })();
+
+ // Name clash (partials)
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls(`
+ interface M { attribute any A; };
+ partial interface M { attribute any A; };`);
+ idlArray.merge_partials();
+ })();
+ </script>
+ <script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "name": "Partial interface A: original interface defined",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: Original interface should be defined expected true got false"
+ },
+ {
+ "name": "Partial interface B: original interface defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial interface B: valid exposure set",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial interface C: original interface defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial interface C: valid exposure set",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial interface D: original interface defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial interface D: valid exposure set",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "Partial D interface is exposed to 'DedicatedWorker', the original interface is not."
+ },
+ {
+ "name": "Partial interface E: original interface defined",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: Original E definition should have type interface expected true got false"
+ },
+ {
+ "name": "partial mixin dep implications",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial interface M: original interface defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial interface M: member names are unique",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: member A is unique expected true got false"
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_primary_interface_of.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_primary_interface_of.html
new file mode 100644
index 0000000000..309de60bb7
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_primary_interface_of.html
@@ -0,0 +1,116 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>idlharness: Primary interface</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+</head>
+<body>
+<p>Verify the series of sub-tests that are executed for "tested" interface
+objects but skipped for "untested" interface objects.</p>
+<script>
+"use strict";
+
+function FooParent() {
+ if (!new.target) {
+ throw new TypeError('FooParent() must be called with new');
+ }
+}
+Object.defineProperty(window, "Foo", {
+ enumerable: false,
+ writable: true,
+ configurable: true,
+ value: function Foo() {
+ if (!new.target) {
+ throw new TypeError('Foo() must be called with new');
+ }
+ }
+ });
+Object.defineProperty(window.Foo, "prototype", {
+ writable: false,
+ value: new FooParent()
+ });
+Object.defineProperty(window.Foo.prototype, "constructor", {
+ enumerable: false,
+ writable: true,
+ configurable: true,
+ value: window.Foo
+ });
+Object.setPrototypeOf(Foo, FooParent);
+Foo.prototype[Symbol.toStringTag] = "Foo";
+
+var idlArray = new IdlArray();
+idlArray.add_untested_idls("interface FooParent {};");
+idlArray.add_idls(
+ "interface Foo : FooParent { constructor(); };"
+ );
+idlArray.add_objects({
+ Foo: ["new Foo()"],
+ FooParent: ["new FooParent()"]
+});
+idlArray.test();
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "name": "Foo interface object length",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface object name",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: existence and properties of interface object",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: existence and properties of interface prototype object",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: existence and properties of interface prototype object's \"constructor\" property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: existence and properties of interface prototype object's @@unscopables property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo must be primary interface of new Foo()",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Stringification of new Foo()",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_to_json_operation.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_to_json_operation.html
new file mode 100644
index 0000000000..bbc502a313
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_to_json_operation.html
@@ -0,0 +1,177 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>IdlInterface.prototype.test_to_json_operation()</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+ <script src="../../../../idl-helper.js"></script>
+</head>
+<body>
+<script>
+ "use strict";
+ function wrap(member, obj) {
+ function F(obj) {
+ this._obj = obj;
+ }
+
+ F.prototype.toJSON = function() {
+ return this._obj;
+ }
+ Object.defineProperty(F, 'name', { value: member.name });
+ return new F(obj);
+ }
+
+ var i, obj;
+ i = interfaceFrom("interface A { [Default] object toJSON(); attribute long foo; };");
+ i.test_to_json_operation("object", wrap(i, { foo: 123 }), i.members[0]);
+
+ // should fail (wrong type)
+ i = interfaceFrom("interface B { [Default] object toJSON(); attribute long foo; };");
+ i.test_to_json_operation("object", wrap(i, { foo: "a value" }), i.members[0]);
+
+ // should handle extraneous attributes (e.g. from an extension specification)
+ i = interfaceFrom("interface C { [Default] object toJSON(); attribute long foo; };");
+ i.test_to_json_operation("object", wrap(i, { foo: 123, bar: 456 }), i.members[0]);
+
+ // should fail (missing property)
+ i = interfaceFrom("interface D { [Default] object toJSON(); attribute long foo; };");
+ i.test_to_json_operation("object", wrap(i, { }), i.members[0]);
+
+ // should fail (should be writable)
+ obj = Object.defineProperties({}, { foo: {
+ writable: false,
+ enumerable: true,
+ configurable: true,
+ value: 123
+ }});
+ i = interfaceFrom("interface F { [Default] object toJSON(); attribute long foo; };");
+ i.test_to_json_operation("object", wrap(i, obj), i.members[0]);
+
+ // should fail (should be enumerable)
+ obj = Object.defineProperties({}, { foo: {
+ writable: true,
+ enumerable: false,
+ configurable: true,
+ value: 123
+ }});
+ i = interfaceFrom("interface G { [Default] object toJSON(); attribute long foo; };");
+ i.test_to_json_operation("object", wrap(i, obj), i.members[0]);
+
+ // should fail (should be configurable)
+ obj = Object.defineProperties({}, { foo: {
+ writable: true,
+ enumerable: true,
+ configurable: false,
+ value: 123
+ }});
+ i = interfaceFrom("interface H { [Default] object toJSON(); attribute long foo; };");
+ i.test_to_json_operation("object", wrap(i, obj), i.members[0]);
+
+ var idl = new IdlArray();
+ idl.add_idls("interface I : J { [Default] object toJSON(); attribute long foo; };");
+ idl.add_idls("interface J { [Default] object toJSON(); attribute DOMString foo;};");
+ var i = idl.members.I;
+ i.test_to_json_operation("object", wrap(i, { foo: 123 }), i.members[0]);
+
+ i = interfaceFrom("interface K { [Default] object toJSON(); };");
+ i.test_to_json_operation("object", wrap(i, {}), i.members[0]);
+
+ i = interfaceFrom("interface L { DOMString toJSON(); };");
+ i.test_to_json_operation("object", wrap(i, "a string"), i.members[0]);
+
+ // should fail (wrong output type)
+ i = interfaceFrom("interface M { DOMString toJSON(); };");
+ i.test_to_json_operation("object", wrap(i, {}), i.members[0]);
+
+ // should fail (not an IDL type)
+ i = interfaceFrom("interface N { DOMException toJSON(); };");
+ i.test_to_json_operation("object", wrap(i, {}), i.members[0]);
+</script>
+<script type="text/json" id="expected">
+ {
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "message": null,
+ "name": "A interface: default toJSON operation on object",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": "assert_equals: expected \"number\" but got \"string\"",
+ "name": "B interface: default toJSON operation on object",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": null,
+ "name": "C interface: default toJSON operation on object",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": "assert_true: property \"foo\" should be present in the output of D.prototype.toJSON() expected true got false",
+ "name": "D interface: default toJSON operation on object",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": "assert_true: property foo should be writable expected true got false",
+ "name": "F interface: default toJSON operation on object",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": "assert_true: property foo should be enumerable expected true got false",
+ "name": "G interface: default toJSON operation on object",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": "assert_true: property foo should be configurable expected true got false",
+ "name": "H interface: default toJSON operation on object",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": null,
+ "name": "I interface: default toJSON operation on object",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "K interface: default toJSON operation on object",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "L interface: toJSON operation on object",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": "assert_equals: expected \"string\" but got \"object\"",
+ "name": "M interface: toJSON operation on object",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": "assert_true: {\"type\":\"return-type\",\"extAttrs\":[],\"generic\":\"\",\"nullable\":false,\"union\":false,\"idlType\":\"DOMException\"} is not an appropriate return value for the toJSON operation of N expected true got false",
+ "name": "N interface: toJSON operation on object",
+ "properties": {},
+ "status_string": "FAIL"
+ }
+ ],
+ "type": "complete"
+ }
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_attribute.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_attribute.html
new file mode 100644
index 0000000000..2c94061fc1
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_attribute.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>idlharness: namespace attribute</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+</head>
+<body>
+<p>Verify the series of sub-tests that are executed for namespace attributes.</p>
+<script>
+"use strict";
+
+Object.defineProperty(self, "foo", {
+ value: {
+ truth: true,
+ },
+ writable: true,
+ enumerable: false,
+ configurable: true,
+});
+
+var idlArray = new IdlArray();
+idlArray.add_idls(`
+[Exposed=Window]
+namespace foo {
+ readonly attribute bool truth;
+ readonly attribute bool lies;
+};`);
+idlArray.test();
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "name": "foo namespace: extended attributes",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: property descriptor",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: [[Extensible]] is true",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: [[Prototype]] is Object.prototype",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: typeof is \"object\"",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: has no length property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: has no name property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: attribute truth",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: attribute lies",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_own_property: foo does not have property \"lies\" expected property \"lies\" missing"
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_operation.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_operation.html
new file mode 100644
index 0000000000..da70c8fa31
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_operation.html
@@ -0,0 +1,242 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>idlharness: namespace operation</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+</head>
+<body>
+<p>Verify the series of sub-tests that are executed for namespace operations.</p>
+<script>
+"use strict";
+
+Object.defineProperty(self, "foo", {
+ value: Object.defineProperty({}, "Truth", {
+ value: function() {},
+ writable: true,
+ enumerable: true,
+ configurable: true,
+ }),
+ writable: true,
+ enumerable: false,
+ configurable: true,
+});
+
+Object.defineProperty(self, "bar", {
+ value: Object.defineProperty({}, "Truth", {
+ value: function() {},
+ writable: false,
+ enumerable: true,
+ configurable: false,
+ }),
+ writable: true,
+ enumerable: false,
+ configurable: true,
+});
+
+Object.defineProperty(self, "baz", {
+ value: {
+ LongStory: function(hero, ...details) {
+ return `${hero} went and ${details.join(', then')}`
+ },
+ ShortStory: function(...details) {
+ return `${details.join('. ')}`;
+ },
+ },
+ writable: true,
+ enumerable: false,
+ configurable: true,
+});
+
+var idlArray = new IdlArray();
+idlArray.add_idls(`
+[Exposed=Window]
+namespace foo {
+ undefined Truth();
+ undefined Lies();
+};
+[Exposed=Window]
+namespace bar {
+ [LegacyUnforgeable]
+ undefined Truth();
+};
+[Exposed=Window]
+namespace baz {
+ DOMString LongStory(any hero, DOMString... details);
+ DOMString ShortStory(DOMString... details);
+};`);
+idlArray.test();
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "name": "foo namespace: extended attributes",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: property descriptor",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: [[Extensible]] is true",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: [[Prototype]] is Object.prototype",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: typeof is \"object\"",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: has no length property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: has no name property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: operation Truth()",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: operation Lies()",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_own_property: namespace object missing operation \"Lies\" expected property \"Lies\" missing"
+ },
+ {
+ "name": "bar namespace: extended attributes",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "bar namespace: property descriptor",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "bar namespace: [[Extensible]] is true",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "bar namespace: [[Prototype]] is Object.prototype",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "bar namespace: typeof is \"object\"",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "bar namespace: has no length property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "bar namespace: has no name property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "bar namespace: operation Truth()",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+
+ {
+ "name": "baz namespace: extended attributes",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "baz namespace: property descriptor",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "baz namespace: [[Extensible]] is true",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "baz namespace: [[Prototype]] is Object.prototype",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "baz namespace: typeof is \"object\"",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "baz namespace: has no length property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "baz namespace: has no name property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "baz namespace: operation LongStory(any, DOMString...)",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "baz namespace: operation ShortStory(DOMString...)",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_partial_namespace.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_partial_namespace.html
new file mode 100644
index 0000000000..eabdcd10a9
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_partial_namespace.html
@@ -0,0 +1,125 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>idlharness: Partial namespace</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+</head>
+
+<body>
+ <p>Verify the series of sub-tests that are executed for "partial" namespace objects.</p>
+ <script>
+ "use strict";
+
+ // No original existence
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls('partial namespace A {};');
+ idlArray.test();
+ })();
+
+ // Valid exposure (Note: Worker -> {Shared,Dedicated,Service}Worker)
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_untested_idls(`
+ [Exposed=(Worker)]
+ namespace B {};
+
+ [Exposed=(DedicatedWorker, ServiceWorker, SharedWorker)]
+ namespace C {};`);
+ idlArray.add_idls(`
+ [Exposed=(DedicatedWorker, ServiceWorker, SharedWorker)]
+ partial namespace B {};
+
+ [Exposed=(Worker)]
+ partial namespace C {};`);
+ idlArray.merge_partials();
+ })();
+
+ // Invalid exposure
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_untested_idls(`
+ [Exposed=(Window, ServiceWorker)]
+ namespace D {};`);
+ idlArray.add_idls(`
+ [Exposed=(DedicatedWorker)]
+ partial namespace D {};`);
+ idlArray.merge_partials();
+ })();
+
+ // Original is an interface, not a namespace.
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls(`
+ partial namespace E {};
+ interface E {};`);
+ idlArray.merge_partials();
+ })();
+ </script>
+ <script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "name": "Partial namespace A: original namespace defined",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: Original namespace should be defined expected true got false"
+ },
+ {
+ "name": "Partial namespace B: original namespace defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial namespace B: valid exposure set",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial namespace C: original namespace defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial namespace C: valid exposure set",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial namespace D: original namespace defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial namespace D: valid exposure set",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "Partial D namespace is exposed to 'DedicatedWorker', the original namespace is not."
+ },
+ {
+ "name": "Partial namespace E: original namespace defined",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: Original E definition should have type namespace expected true got false"
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/iframe-callback.html b/testing/web-platform/tests/resources/test/tests/functional/iframe-callback.html
new file mode 100644
index 0000000000..f49d0aa6b8
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/iframe-callback.html
@@ -0,0 +1,116 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Example with iframe that notifies containing document via callbacks</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body onload="start_test_in_iframe()">
+<h1>Callbacks From Tests Running In An IFRAME</h1>
+<p>A test is run inside an <tt>iframe</tt> with a same origin document. The
+containing document should receive callbacks as the tests progress inside the
+<tt>iframe</tt>. A single passing test is expected in the summary below.
+<div id="log"></div>
+
+<script>
+var callbacks = [];
+var START = 1
+var TEST_STATE = 2
+var RESULT = 3
+var COMPLETION = 4
+var test_complete = false;
+
+setup({explicit_done: true});
+
+// The following callbacks are called for tests in this document as well as the
+// tests in the IFRAME. Currently, callbacks invoked from this document and any
+// child document are indistinguishable from each other.
+
+function start_callback(properties) {
+ callbacks.push(START);
+}
+
+function test_state_callback(test) {
+ callbacks.push(TEST_STATE);
+}
+
+function result_callback(test) {
+ callbacks.push(RESULT);
+}
+
+function completion_callback(tests, status) {
+ if (test_complete) {
+ return;
+ }
+ test_complete = true;
+ callbacks.push(COMPLETION);
+ verify_received_callbacks();
+ done();
+}
+
+function verify_received_callbacks() {
+ var copy_of_callbacks = callbacks.slice(0);
+
+ // Note that you can't run test assertions directly in a callback even if
+ // this is a file test. When the callback is invoked from a same-origin child
+ // page, the callstack reaches into the calling child document. Any
+ // exception thrown in a callback will be handled by the child rather than
+ // this document.
+ test(
+ function() {
+ // callbacks list should look like:
+ // START 1*(TEST_STATE) RESULT COMPLETION
+ assert_equals(copy_of_callbacks.shift(), START,
+ "The first received callback should be 'start_callback'.");
+ assert_equals(copy_of_callbacks.shift(), TEST_STATE,
+ "'test_state_callback' should be received before any " +
+ "result or completion callbacks.");
+ while(copy_of_callbacks.length > 0) {
+ var callback = copy_of_callbacks.shift();
+ if (callback != TEST_STATE) {
+ copy_of_callbacks.unshift(callback);
+ break;
+ }
+ }
+ assert_equals(copy_of_callbacks.shift(), RESULT,
+ "'test_state_callback' should be followed by 'result_callback'.");
+ assert_equals(copy_of_callbacks.shift(), COMPLETION,
+ "Final 'result_callback' should be followed by 'completion_callback'.");
+ assert_equals(copy_of_callbacks.length, 0,
+ "'completion_callback' should be the last callback.");
+ });
+}
+
+function start_test_in_iframe() {
+ // This document is going to clear any received callbacks and maintain
+ // radio silence until the test in the iframe runs to completion. The
+ // completion_callback() will then complete the testing on this document.
+ callbacks.length = 0;
+ var iframe = document.createElement("iframe");
+ // single-page-test-pass.html has a single test.
+ iframe.src = "single-page-test-pass.html";
+ iframe.style.setProperty("display", "none");
+ document.getElementById("target").appendChild(iframe);
+}
+</script>
+
+<div id="target">
+</div>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Example with iframe that notifies containing document via callbacks",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/iframe-consolidate-errors.html b/testing/web-platform/tests/resources/test/tests/functional/iframe-consolidate-errors.html
new file mode 100644
index 0000000000..ef9b8702ec
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/iframe-consolidate-errors.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Example with iframe that consolidates errors via fetch_tests_from_window</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+var parent_test = async_test("Test executing in parent context");
+</script>
+</head>
+<body onload="parent_test.done()">
+<h1>Fetching Tests From a Child Context</h1>
+<p>This test demonstrates the use of <tt>fetch_tests_from_window</tt> to pull
+tests from an <tt>iframe</tt> into the primary document.</p>
+<p>The test suite is expected to fail due to an unhandled exception in the
+child context.</p>
+<div id="log"></div>
+
+<iframe id="childContext" src="uncaught-exception-handle.html" style="display:none"></iframe>
+<!-- apisample4.html is a failing suite due to an unhandled Error. -->
+
+<script>
+ var childContext = document.getElementById("childContext");
+ fetch_tests_from_window(childContext.contentWindow);
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Error in remote: Error: Example Error"
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Test executing in parent context",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "This should show a harness status of 'Error' and a test status of 'Not Run'",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/iframe-consolidate-tests.html b/testing/web-platform/tests/resources/test/tests/functional/iframe-consolidate-tests.html
new file mode 100644
index 0000000000..246dddee11
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/iframe-consolidate-tests.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Example with iframe that consolidates tests via fetch_tests_from_window</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+var parent_test = async_test("Test executing in parent context");
+</script>
+</head>
+<body onload="parent_test.done()">
+<h1>Fetching Tests From a Child Context</h1>
+<p>This test demonstrates the use of <tt>fetch_tests_from_window</tt> to pull
+tests from an <tt>iframe</tt> into the primary document.</p>
+<p>The test suite will not complete until tests in the child context have finished
+executing</p>
+<div id="log"></div>
+
+<iframe id="childContext" src="promise-async.html" style="display:none"></iframe>
+
+<script>
+ var childContext = document.getElementById("childContext");
+ fetch_tests_from_window(childContext.contentWindow);
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Promise rejection",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Promise resolution",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Promises and test assertion failures (should fail)",
+ "properties": {},
+ "message": "assert_true: This failure is expected expected true got false"
+ },
+ {
+ "status_string": "PASS",
+ "name": "Promises are supported in your browser",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Promises resolution chaining",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test executing in parent context",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Use of step_func with Promises",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Use of unreached_func with Promises (should fail)",
+ "properties": {},
+ "message": "assert_unreached: This failure is expected Reached unreachable code"
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/iframe-msg.html b/testing/web-platform/tests/resources/test/tests/functional/iframe-msg.html
new file mode 100644
index 0000000000..283a5d98cc
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/iframe-msg.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Example with iframe that notifies containing document via cross document messaging</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<h1>Notifications From Tests Running In An IFRAME</h1>
+<p>A test is run inside an <tt>iframe</tt> with a same origin document. The
+containing document should receive messages via <tt>postMessage</tt>/
+<tt>onmessage</tt> as the tests progress inside the <tt>iframe</tt>. A single
+passing test is expected in the summary below.
+</p>
+<div id="log"></div>
+
+<script>
+var t = async_test("Containing document receives messages");
+var start_received = false;
+var result_received = false;
+var completion_received = false;
+
+// These are the messages that are expected to be seen while running the tests
+// in the IFRAME.
+var expected_messages = [
+ t.step_func(
+ function(message) {
+ assert_equals(message.data.type, "start");
+ assert_own_property(message.data, "properties");
+ }),
+
+ t.step_func(
+ function(message) {
+ assert_equals(message.data.type, "test_state");
+ assert_equals(message.data.test.status, message.data.test.NOTRUN);
+ }),
+
+ t.step_func(
+ function(message) {
+ assert_equals(message.data.type, "result");
+ assert_equals(message.data.test.status, message.data.test.PASS);
+ }),
+
+ t.step_func(
+ function(message) {
+ assert_equals(message.data.type, "complete");
+ assert_equals(message.data.tests.length, 1);
+ assert_equals(message.data.tests[0].status,
+ message.data.tests[0].PASS);
+ assert_equals(message.data.status.status, message.data.status.OK);
+ t.done();
+ }),
+
+ t.unreached_func("Too many messages received")
+];
+
+on_event(window,
+ "message",
+ function(message) {
+ var handler = expected_messages.shift();
+ handler(message);
+ });
+</script>
+<iframe src="single-page-test-pass.html" style="display:none">
+ <!-- single-page-test-pass.html implements a file_is_test test. -->
+</iframe>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Containing document receives messages",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/log-insertion.html b/testing/web-platform/tests/resources/test/tests/functional/log-insertion.html
new file mode 100644
index 0000000000..9a63c3dbde
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/log-insertion.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<title>Log insertion</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(function(t) {
+ assert_equals(document.body, null);
+}, "Log insertion before load");
+test(function(t) {
+ assert_equals(document.body, null);
+}, "Log insertion before load (again)");
+async_test(function(t) {
+ window.onload = t.step_func_done(function() {
+ var body = document.body;
+ assert_not_equals(body, null);
+
+ var log = document.getElementById("log");
+ assert_equals(log.parentNode, body);
+ });
+}, "Log insertion after load");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [{
+ "status_string": "PASS",
+ "name": "Log insertion before load",
+ "message": null,
+ "properties": {}
+ }, {
+ "status_string": "PASS",
+ "name": "Log insertion before load (again)",
+ "message": null,
+ "properties": {}
+ }, {
+ "status_string": "PASS",
+ "name": "Log insertion after load",
+ "message": null,
+ "properties": {}
+ }],
+ "type": "complete"
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/no-title.html b/testing/web-platform/tests/resources/test/tests/functional/no-title.html
new file mode 100644
index 0000000000..a337e4e5f5
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/no-title.html
@@ -0,0 +1,146 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Tests with no title</title>
+</head>
+<script src="/resources/testharness.js"></script>
+
+<body>
+<h1>Tests with no title</h1>
+<div id="log"></div>
+<script>
+ test(function(){assert_true(true, '1')});
+ test(()=>assert_true(true, '2'));
+ test(() => assert_true(true, '3'));
+ test(() => assert_true(true, '3')); // test duplicate behaviour
+ test(() => assert_true(true, '3')); // test duplicate behaviour
+ test(() => {
+ assert_true(true, '4');
+ });
+ test(() => { assert_true(true, '5') });
+ test(() => { assert_true(true, '6') } );
+ test(() => { assert_true(true, '7'); });
+ test(() => {});
+ test(() => { });
+ test(() => {;;;;});
+ test(() => { ; ; ; ; });
+ test(()=>{});
+ test(()=>{ });
+ test(()=>{;;;;});
+ test(()=>{ ; ; ; ; });
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_true(true, '2')",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_true(true, '3')",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_true(true, '3') 1",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_true(true, '3') 2",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title 1",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_true(true, '5')",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_true(true, '6')",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_true(true, '7')",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title 2",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title 3",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title 4",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title 5",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title 6",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title 7",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title 8",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title 9",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/order.html b/testing/web-platform/tests/resources/test/tests/functional/order.html
new file mode 100644
index 0000000000..686383861a
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/order.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Ordering</title>
+<meta name="timeout" content="6000">
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(function() {}, 'second');
+test(function() {}, 'first');
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [{
+ "status_string": "PASS",
+ "name": "first",
+ "message": null,
+ "properties": {}
+ }, {
+ "status_string": "PASS",
+ "name": "second",
+ "message": null,
+ "properties": {}
+ }],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/promise-async.html b/testing/web-platform/tests/resources/test/tests/functional/promise-async.html
new file mode 100644
index 0000000000..fa82665cf0
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/promise-async.html
@@ -0,0 +1,172 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Async Tests and Promises</title>
+</head>
+<body>
+<h1>Async Tests and Promises</h1>
+<p>This test assumes ECMAScript 6 Promise support. Some failures are expected.</p>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+
+test(function() {
+ var p = new Promise(function(resolve, reject) {});
+ assert_true('then' in p);
+ assert_equals(typeof Promise.resolve, 'function');
+ assert_equals(typeof Promise.reject, 'function');
+}, "Promises are supported in your browser");
+
+(function() {
+ var t = async_test("Promise resolution");
+ t.step(function() {
+ Promise.resolve('x').then(
+ t.step_func(function(value) {
+ assert_equals(value, 'x');
+ t.done();
+ }),
+ t.unreached_func('Promise should not reject')
+ );
+ });
+}());
+
+(function() {
+ var t = async_test("Promise rejection");
+ t.step(function() {
+ Promise.reject(Error('fail')).then(
+ t.unreached_func('Promise should reject'),
+ t.step_func(function(reason) {
+ assert_true(reason instanceof Error);
+ assert_equals(reason.message, 'fail');
+ t.done();
+ })
+ );
+ });
+}());
+
+(function() {
+ var t = async_test("Promises resolution chaining");
+ t.step(function() {
+ var resolutions = [];
+ Promise.resolve('a').then(
+ t.step_func(function(value) {
+ resolutions.push(value);
+ return 'b';
+ })
+ ).then(
+ t.step_func(function(value) {
+ resolutions.push(value);
+ return 'c';
+ })
+ ).then(
+ t.step_func(function(value) {
+ resolutions.push(value);
+
+ assert_array_equals(resolutions, ['a', 'b', 'c']);
+ t.done();
+ })
+ ).catch(
+ t.unreached_func('promise should not have rejected')
+ );
+ });
+}());
+
+(function() {
+ var t = async_test("Use of step_func with Promises");
+ t.step(function() {
+ var resolutions = [];
+ Promise.resolve('x').then(
+ t.step_func_done(),
+ t.unreached_func('Promise should not have rejected')
+ );
+ });
+}());
+
+(function() {
+ var t = async_test("Promises and test assertion failures (should fail)");
+ t.step(function() {
+ var resolutions = [];
+ Promise.resolve('x').then(
+ t.step_func(function(value) {
+ assert_true(false, 'This failure is expected');
+ })
+ ).then(
+ t.unreached_func('Promise should not have resolved')
+ ).catch(
+ t.unreached_func('Promise should not have rejected')
+ );
+ });
+}());
+
+(function() {
+ var t = async_test("Use of unreached_func with Promises (should fail)");
+ t.step(function() {
+ var resolutions = [];
+ var r;
+ var p = new Promise(function(resolve, reject) {
+ // Reject instead of resolve, to demonstrate failure.
+ reject(123);
+ });
+ p.then(
+ function(value) {
+ assert_equals(value, 123, 'This should not actually happen');
+ },
+ t.unreached_func('This failure is expected')
+ );
+ });
+}());
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Promise rejection",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Promise resolution",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Promises and test assertion failures (should fail)",
+ "properties": {},
+ "message": "assert_true: This failure is expected expected true got false"
+ },
+ {
+ "status_string": "PASS",
+ "name": "Promises are supported in your browser",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Promises resolution chaining",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Use of step_func with Promises",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Use of unreached_func with Promises (should fail)",
+ "properties": {},
+ "message": "assert_unreached: This failure is expected Reached unreachable code"
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/promise-with-sync.html b/testing/web-platform/tests/resources/test/tests/functional/promise-with-sync.html
new file mode 100644
index 0000000000..e8e680a9c7
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/promise-with-sync.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Promise Tests and Synchronous Tests</title>
+</head>
+<body>
+<h1>Promise Tests</h1>
+<p>This test demonstrates the use of <tt>promise_test</tt> alongside synchronous tests.</p>
+<div id="log"></div>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+<script>
+"use strict";
+var sequence = [];
+
+test(function(t) {
+ assert_array_equals(sequence, []);
+ sequence.push(1);
+}, "first synchronous test");
+
+promise_test(function() {
+ assert_array_equals(sequence, [1, 2]);
+
+ return Promise.resolve()
+ .then(function() {
+ assert_array_equals(sequence, [1, 2]);
+ sequence.push(3);
+ });
+}, "first promise_test");;
+
+test(function(t) {
+ assert_array_equals(sequence, [1]);
+ sequence.push(2);
+}, "second synchronous test");
+
+promise_test(function() {
+ assert_array_equals(sequence, [1, 2, 3]);
+
+ return Promise.resolve()
+ .then(function() {
+ assert_array_equals(sequence, [1, 2, 3]);
+ });
+}, "second promise_test");;
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "message": null,
+ "properties": {},
+ "name": "first promise_test",
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "properties": {},
+ "name": "first synchronous test",
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "properties": {},
+ "name": "second promise_test",
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "properties": {},
+ "name": "second synchronous test",
+ "status_string": "PASS"
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/promise.html b/testing/web-platform/tests/resources/test/tests/functional/promise.html
new file mode 100644
index 0000000000..f35feb0e21
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/promise.html
@@ -0,0 +1,219 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Promise Tests</title>
+</head>
+<body>
+<h1>Promise Tests</h1>
+<p>This test demonstrates the use of <tt>promise_test</tt>. Assumes ECMAScript 6
+Promise support. Some failures are expected.</p>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(
+ function() {
+ var p = new Promise(function(resolve, reject){});
+ assert_true("then" in p);
+ assert_equals(typeof Promise.resolve, "function");
+ assert_equals(typeof Promise.reject, "function");
+ },
+ "Promises are supported in your browser");
+
+promise_test(
+ function() {
+ return Promise.resolve("x")
+ .then(
+ function(value) {
+ assert_equals(value,
+ "x",
+ "Fulfilled promise should pass result to " +
+ "fulfill reaction.");
+ });
+ },
+ "Promise fulfillment with result");
+
+promise_test(
+ function(t) {
+ return Promise.reject(new Error("fail"))
+ .then(t.unreached_func("Promise should reject"),
+ function(reason) {
+ assert_true(
+ reason instanceof Error,
+ "Rejected promise should pass reason to fulfill reaction.");
+ assert_equals(
+ reason.message,
+ "fail",
+ "Rejected promise should pass reason to reject reaction.");
+ });
+ },
+ "Promise rejection with result");
+
+promise_test(
+ function() {
+ var resolutions = [];
+ return Promise.resolve("a")
+ .then(
+ function(value) {
+ resolutions.push(value);
+ return "b";
+ })
+ .then(
+ function(value) {
+ resolutions.push(value);
+ return "c";
+ })
+ .then(
+ function(value) {
+ resolutions.push(value);
+ assert_array_equals(resolutions, ["a", "b", "c"]);
+ });
+ },
+ "Chain of promise resolutions");
+
+promise_test(
+ function(t) {
+ var resolutions = [];
+ return Promise.resolve("x")
+ .then(
+ function(value) {
+ assert_true(false, "Expected failure.");
+ })
+ .then(t.unreached_func("UNEXPECTED FAILURE: Promise should not have resolved."));
+ },
+ "Assertion failure in a fulfill reaction (should FAIL with an expected failure)");
+
+promise_test(
+ function(t) {
+ return new Promise(
+ function(resolve, reject) {
+ reject(123);
+ })
+ .then(t.unreached_func("UNEXPECTED FAILURE: Fulfill reaction reached after rejection."),
+ t.unreached_func("Expected failure."));
+ },
+ "unreached_func as reactor (should FAIL with an expected failure)");
+
+promise_test(
+ function() {
+ return true;
+ },
+ "promise_test with function that doesn't return a Promise (should FAIL)");
+
+promise_test(function(){},
+ "promise_test with function that doesn't return anything");
+
+promise_test(
+ function() { return { then: 23 }; },
+ "promise_test that returns a non-thenable (should FAIL)");
+
+promise_test(
+ function() {
+ return Promise.reject("Expected rejection");
+ },
+ "promise_test with unhandled rejection (should FAIL)");
+
+promise_test(
+ function() {
+ return Promise.resolve(10)
+ .then(
+ function(value) {
+ throw Error("Expected exception.");
+ });
+ },
+ "promise_test with unhandled exception in fulfill reaction (should FAIL)");
+
+promise_test(
+ function(t) {
+ return Promise.reject(10)
+ .then(
+ t.unreached_func("UNEXPECTED FAILURE: Fulfill reaction reached after rejection."),
+ function(value) {
+ throw Error("Expected exception.");
+ });
+ },
+ "promise_test with unhandled exception in reject reaction (should FAIL)");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "FAIL",
+ "name": "Assertion failure in a fulfill reaction (should FAIL with an expected failure)",
+ "message": "assert_true: Expected failure. expected true got false",
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Chain of promise resolutions",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Promise fulfillment with result",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Promise rejection with result",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Promises are supported in your browser",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "promise_test with function that doesn't return a Promise (should FAIL)",
+ "message": "promise_test: test body must return a 'thenable' object (received an object with no `then` method)",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "promise_test with function that doesn't return anything",
+ "message": "promise_test: test body must return a 'thenable' object (received undefined)",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "promise_test that returns a non-thenable (should FAIL)",
+ "message": "promise_test: test body must return a 'thenable' object (received an object with no `then` method)",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "promise_test with unhandled exception in fulfill reaction (should FAIL)",
+ "message": "promise_test: Unhandled rejection with value: object \"Error: Expected exception.\"",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "promise_test with unhandled exception in reject reaction (should FAIL)",
+ "message": "promise_test: Unhandled rejection with value: object \"Error: Expected exception.\"",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "promise_test with unhandled rejection (should FAIL)",
+ "message": "promise_test: Unhandled rejection with value: \"Expected rejection\"",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "unreached_func as reactor (should FAIL with an expected failure)",
+ "message": "assert_unreached: Expected failure. Reached unreachable code",
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/queue.html b/testing/web-platform/tests/resources/test/tests/functional/queue.html
new file mode 100644
index 0000000000..0c721286ec
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/queue.html
@@ -0,0 +1,130 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test queuing synchronous tests</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+
+<script>
+"use strict";
+var inInitialTurn = true;
+
+test(function(t) {
+ assert_true(
+ inInitialTurn, "should execute in the initial turn of the event loop"
+ );
+}, "First synchronous test");
+
+test(function(t) {
+ assert_true(
+ inInitialTurn, "should execute in the initial turn of the event loop"
+ );
+}, "Second synchronous test");
+
+async_test(function(t) {
+ assert_true(
+ inInitialTurn, "should execute in the initial turn of the event loop"
+ );
+ t.done();
+}, "First async_test (run in parallel)");
+
+async_test(function(t) {
+ assert_true(
+ inInitialTurn, "should execute in the initial turn of the event loop"
+ );
+ t.done();
+}, "Second async_test (run in parallel)");
+
+test(function(t) {
+ assert_true(
+ inInitialTurn, "should execute in the initial turn of the event loop"
+ );
+}, "Third synchronous test");
+
+promise_test(function(t) {
+ assert_false(
+ inInitialTurn, "should not execute in the initial turn of the event loop"
+ );
+
+ return Promise.resolve();
+}, "promise_test");
+
+async_test(function(t) {
+ assert_true(
+ inInitialTurn, "should execute in the initial turn of the event loop"
+ );
+ t.done();
+}, "Third async_test (run in parallel)");
+
+test(function(t) {
+ assert_true(
+ inInitialTurn, "should execute in the initial turn of the event loop"
+ );
+}, "Fourth synchronous test");
+
+inInitialTurn = false;
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "properties": {},
+ "name": "First async_test (run in parallel)",
+ "status_string": "PASS",
+ "message": null
+ },
+ {
+ "properties": {},
+ "name": "First synchronous test",
+ "status_string": "PASS",
+ "message": null
+ },
+ {
+ "properties": {},
+ "name": "Fourth synchronous test",
+ "status_string": "PASS",
+ "message": null
+ },
+ {
+ "properties": {},
+ "name": "Second async_test (run in parallel)",
+ "status_string": "PASS",
+ "message": null
+ },
+ {
+ "properties": {},
+ "name": "Second synchronous test",
+ "status_string": "PASS",
+ "message": null
+ },
+ {
+ "properties": {},
+ "name": "Third async_test (run in parallel)",
+ "status_string": "PASS",
+ "message": null
+ },
+ {
+ "properties": {},
+ "name": "Third synchronous test",
+ "status_string": "PASS",
+ "message": null
+ },
+ {
+ "properties": {},
+ "name": "promise_test",
+ "status_string": "PASS",
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/setup-function-worker.js b/testing/web-platform/tests/resources/test/tests/functional/setup-function-worker.js
new file mode 100644
index 0000000000..82c1456aa6
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/setup-function-worker.js
@@ -0,0 +1,14 @@
+importScripts("/resources/testharness.js");
+
+// Regression test for https://github.com/web-platform-tests/wpt/issues/27299,
+// where we broke the ability for a setup function in a worker to contain an
+// assertion (even a passing one).
+setup(function() {
+ assert_true(true, "True is true");
+});
+
+// We must define at least one test for the harness, though it is not what we
+// are testing here.
+test(function() {
+ assert_false(false, "False is false");
+}, 'Worker test');
diff --git a/testing/web-platform/tests/resources/test/tests/functional/setup-worker-service.html b/testing/web-platform/tests/resources/test/tests/functional/setup-worker-service.html
new file mode 100644
index 0000000000..9f24adac2d
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/setup-worker-service.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Setup function in a service worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<h1>Setup function in a service worker</h1>
+<p>This test assumes that the browser supports <a href="http://www.w3.org/TR/service-workers/">ServiceWorkers</a>.
+<div id="log"></div>
+
+<script>
+test(function(t) {
+ assert_true("serviceWorker" in navigator,
+ "navigator.serviceWorker exists");
+}, "Browser supports ServiceWorker");
+
+promise_test(function() {
+ // Since the service worker registration could be in an indeterminate
+ // state (due to, for example, a previous test run failing), we start by
+ // unregstering our service worker and then registering it again.
+ var scope = "service-worker-scope";
+ var worker_url = "setup-function-worker.js";
+
+ return navigator.serviceWorker.register(worker_url, {scope: scope})
+ .then(function(registration) {
+ return registration.unregister();
+ }).then(function() {
+ return navigator.serviceWorker.register(worker_url, {scope: scope});
+ }).then(function(registration) {
+ add_completion_callback(function() {
+ registration.unregister();
+ });
+
+ return new Promise(function(resolve) {
+ registration.addEventListener("updatefound", function() {
+ resolve(registration.installing);
+ });
+ });
+ }).then(function(worker) {
+ fetch_tests_from_worker(worker);
+ });
+}, "Register ServiceWorker");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Browser supports ServiceWorker",
+ "properties": {},
+ "message": null
+ },
+ {
+ "message": null,
+ "name": "Register ServiceWorker",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "Worker test",
+ "properties": {},
+ "status_string": "PASS"
+ }
+ ],
+ "summarized_asserts": [
+ {
+ "assert_name": "assert_true",
+ "test": "Browser supports ServiceWorker",
+ "args": [
+ "true",
+ "\"navigator.serviceWorker exists\""
+ ],
+ "status": 0
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/single-page-test-fail.html b/testing/web-platform/tests/resources/test/tests/functional/single-page-test-fail.html
new file mode 100644
index 0000000000..8bbd530c48
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/single-page-test-fail.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<title>Example with file_is_test (should fail)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+setup({ single_test: true });
+onload = function() {
+ assert_true(false);
+ done();
+}
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "FAIL",
+ "name": "Example with file_is_test (should fail)",
+ "properties": {},
+ "message": "uncaught exception: Error: assert_true: expected true got false"
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/single-page-test-no-assertions.html b/testing/web-platform/tests/resources/test/tests/functional/single-page-test-no-assertions.html
new file mode 100644
index 0000000000..9b39d2a02c
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/single-page-test-no-assertions.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<title>Example single page test with no asserts</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+setup({ single_test: true });
+done();
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Example single page test with no asserts",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/single-page-test-no-body.html b/testing/web-platform/tests/resources/test/tests/functional/single-page-test-no-body.html
new file mode 100644
index 0000000000..cb018f4dae
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/single-page-test-no-body.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<title>Example single page test with no body</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+setup({ single_test: true });
+assert_true(true);
+done();
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Example single page test with no body",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/single-page-test-pass.html b/testing/web-platform/tests/resources/test/tests/functional/single-page-test-pass.html
new file mode 100644
index 0000000000..e143e22f3c
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/single-page-test-pass.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<title>Example with file_is_test</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+setup({ single_test: true });
+onload = function() {
+ assert_true(true);
+ done();
+}
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Example with file_is_test",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/step_wait.html b/testing/web-platform/tests/resources/test/tests/functional/step_wait.html
new file mode 100644
index 0000000000..ae3442c759
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/step_wait.html
@@ -0,0 +1,57 @@
+<!doctype html>
+<title>Tests for step_wait</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+promise_test(async t => {
+ let x = 1;
+ Promise.resolve().then(() => ++x);
+ await t.step_wait(() => x === 1);
+ assert_equals(x, 2);
+}, "Basic step_wait() test");
+
+promise_test(async t => {
+ let cond = false;
+ let x = 0;
+ setTimeout(() => cond = true, 100);
+ await t.step_wait(() => {
+ ++x;
+ return cond;
+ });
+ assert_equals(x, 2);
+}, "step_wait() isn't invoked too often");
+
+promise_test(async t => {
+ await t.step_wait(); // Throws
+}, "step_wait() takes an argument");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_tests": [
+ {
+ "name": "Basic step_wait() test",
+ "message": null,
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "name": "step_wait() isn't invoked too often",
+ "message": null,
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "name": "step_wait() takes an argument",
+ "message": "cond is not a function",
+ "properties": {},
+ "status_string": "FAIL"
+ }
+ ],
+ "type": "complete",
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ }
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/step_wait_func.html b/testing/web-platform/tests/resources/test/tests/functional/step_wait_func.html
new file mode 100644
index 0000000000..9fed18a3e2
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/step_wait_func.html
@@ -0,0 +1,49 @@
+<!doctype html>
+<title>Tests for step_wait_func and step_wait_func_done</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+async_test(t => {
+ let x = 0;
+ let step_x = 0;
+ setTimeout(() => ++x, 100);
+ t.step_wait_func(() => {
+ ++step_x;
+ return x === 1;
+ }, () => {
+ assert_equals(step_x, 2);
+ t.done();
+ });
+}, "Basic step_wait_func() test");
+
+async_test(t => {
+ let x = 0;
+ setTimeout(() => ++x, 100);
+ t.step_wait_func_done(() => true, () => assert_equals(x, 0));
+}, "Basic step_wait_func_done() test");
+
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "properties": {},
+ "message": null,
+ "name": "Basic step_wait_func() test",
+ "status_string": "PASS"
+ },
+ {
+ "properties": {},
+ "message": null,
+ "name": "Basic step_wait_func_done() test",
+ "status_string": "PASS"
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/task-scheduling-promise-test.html b/testing/web-platform/tests/resources/test/tests/functional/task-scheduling-promise-test.html
new file mode 100644
index 0000000000..9d8e5c11cc
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/task-scheduling-promise-test.html
@@ -0,0 +1,241 @@
+<!doctype html>
+<title>testharness.js - task scheduling</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+<script>
+var sameTask = null;
+var sameMicrotask = null;
+var expectedError = new Error('This error is expected');
+
+promise_test(function() {
+ return Promise.resolve()
+ .then(function() {
+ sameMirotask = true;
+ Promise.resolve().then(() => sameMicrotask = false);
+ });
+}, 'promise test without cleanup #1');
+
+promise_test(function() {
+ assert_false(sameMicrotask);
+
+ return Promise.resolve();
+}, 'sub-test with 0 cleanup functions executes in distinct microtask from a passing sub-test');
+
+promise_test(function() {
+ return Promise.resolve()
+ .then(function() {
+ sameMirotask = true;
+ Promise.resolve().then(() => sameMicrotask = false);
+ throw expectedError;
+ });
+}, 'failing promise test without cleanup #1');
+
+promise_test(function() {
+ assert_false(sameMicrotask);
+
+ return Promise.resolve();
+}, 'sub-test with 0 cleanup functions executes in distinct microtask from a failing sub-test');
+
+promise_test(function(t) {
+ t.add_cleanup(function() {});
+
+ return Promise.resolve()
+ .then(function() {
+ sameMirotask = true;
+ Promise.resolve().then(() => sameMicrotask = false);
+ });
+}, 'promise test with cleanup #1');
+
+promise_test(function() {
+ assert_false(sameMicrotask);
+
+ return Promise.resolve();
+}, 'sub-test with some cleanup functions executes in distinct microtask from a passing sub-test');
+
+promise_test(function(t) {
+ t.add_cleanup(function() {});
+
+ return Promise.resolve()
+ .then(function() {
+ sameMirotask = true;
+ Promise.resolve().then(() => sameMicrotask = false);
+ throw expectedError;
+ });
+}, 'failing promise test with cleanup #1');
+
+promise_test(function() {
+ assert_false(sameMicrotask);
+
+ return Promise.resolve();
+}, 'sub-test with some cleanup functions executes in distinct microtask from a failing sub-test');
+
+promise_test(function(t) {
+ return Promise.resolve()
+ .then(function() {
+ sameTask = true;
+ t.step_timeout(() => sameTask = false, 0);
+ });
+}, 'promise test without cleanup #2');
+
+promise_test(function() {
+ assert_true(sameTask);
+
+ return Promise.resolve();
+}, 'sub-test with 0 cleanup functions executes in the same task as a passing sub-test');
+
+promise_test(function(t) {
+ return Promise.resolve()
+ .then(function() {
+ sameTask = true;
+ t.step_timeout(() => sameTask = false, 0);
+ throw expectedError;
+ });
+}, 'failing promise test without cleanup #2');
+
+promise_test(function() {
+ assert_true(sameTask);
+
+ return Promise.resolve();
+}, 'sub-test with 0 cleanup functions executes in the same task as a failing sub-test');
+
+promise_test(function(t) {
+ t.add_cleanup(function() {});
+
+ return Promise.resolve()
+ .then(function() {
+ sameTask = true;
+ t.step_timeout(() => sameTask = false, 0);
+ });
+}, 'promise test with cleanup #2');
+
+promise_test(function() {
+ assert_true(sameTask);
+
+ return Promise.resolve();
+}, 'sub-test with some cleanup functions executes in the same task as a passing sub-test');
+
+promise_test(function(t) {
+ t.add_cleanup(function() {});
+
+ return Promise.resolve()
+ .then(function() {
+ sameTask = true;
+ t.step_timeout(() => sameTask = false, 0);
+ throw expectedError;
+ });
+}, 'failing promise test with cleanup #2');
+
+promise_test(function() {
+ assert_true(sameTask);
+
+ return Promise.resolve();
+}, 'sub-test with some cleanup functions executes in the same task as a failing sub-test');
+</script>
+
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "message": "promise_test: Unhandled rejection with value: object \"Error: This error is expected\"",
+ "name": "failing promise test with cleanup #1",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": "promise_test: Unhandled rejection with value: object \"Error: This error is expected\"",
+ "name": "failing promise test with cleanup #2",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": "promise_test: Unhandled rejection with value: object \"Error: This error is expected\"",
+ "name": "failing promise test without cleanup #1",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": "promise_test: Unhandled rejection with value: object \"Error: This error is expected\"",
+ "name": "failing promise test without cleanup #2",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": null,
+ "name": "promise test with cleanup #1",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "promise test with cleanup #2",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "promise test without cleanup #1",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "promise test without cleanup #2",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with 0 cleanup functions executes in distinct microtask from a failing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with 0 cleanup functions executes in distinct microtask from a passing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with 0 cleanup functions executes in the same task as a failing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with 0 cleanup functions executes in the same task as a passing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with some cleanup functions executes in distinct microtask from a failing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with some cleanup functions executes in distinct microtask from a passing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with some cleanup functions executes in the same task as a failing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with some cleanup functions executes in the same task as a passing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/task-scheduling-test.html b/testing/web-platform/tests/resources/test/tests/functional/task-scheduling-test.html
new file mode 100644
index 0000000000..035844448d
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/task-scheduling-test.html
@@ -0,0 +1,141 @@
+<!doctype html>
+<title>testharness.js - task scheduling</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+<script>
+var sameMicrotask = null;
+var expectedError = new Error('This error is expected');
+
+// Derived from `immediate`
+// https://github.com/calvinmetcalf/immediate/blob/c353bd2106648cee1d525bfda22cfc4456e69c0e/lib/mutation.js
+function microTask(callback) {
+ var observer = new MutationObserver(callback);
+ var element = document.createTextNode('');
+ observer.observe(element, {
+ characterData: true
+ });
+
+ element.data = true;
+};
+
+async_test(function(t) {
+ var microtask_ran = false;
+
+ t.step_timeout(t.step_func(function() {
+ assert_true(microtask_ran, 'function registered as a microtask was executed before task');
+ t.done();
+ }), 0);
+
+ microTask(function() {
+ microtask_ran = true;
+ });
+}, 'precondition: microtask creation logic functions as expected');
+
+test(function() {
+ sameMicrotask = true;
+ microTask(function() { sameMicrotask = false; });
+}, 'synchronous test without cleanup');
+
+test(function() {
+ assert_true(sameMicrotask);
+}, 'sub-test with 0 cleanup functions executes in the same microtask as a passing sub-test');
+
+test(function() {
+ sameMicrotask = true;
+ microTask(function() { sameMicrotask = false; });
+ throw expectedError;
+}, 'failing synchronous test without cleanup');
+
+test(function() {
+ assert_true(sameMicrotask);
+}, 'sub-test with 0 cleanup functions executes in the same microtask as a failing sub-test');
+
+test(function(t) {
+ t.add_cleanup(function() {});
+
+ sameMicrotask = true;
+ microTask(function() { sameMicrotask = false; });
+}, 'synchronous test with cleanup');
+
+test(function() {
+ assert_true(sameMicrotask);
+}, 'sub-test with some cleanup functions executes in the same microtask as a passing sub-test');
+
+test(function(t) {
+ t.add_cleanup(function() {});
+
+ sameMicrotask = true;
+ microTask(function() { sameMicrotask = false; });
+ throw expectedError;
+}, 'failing synchronous test with cleanup');
+
+test(function() {
+ assert_true(sameMicrotask);
+}, 'sub-test with some cleanup functions executes in the same microtask as a failing sub-test');
+</script>
+
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "message": "This error is expected",
+ "name": "failing synchronous test with cleanup",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": "This error is expected",
+ "name": "failing synchronous test without cleanup",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": null,
+ "name": "precondition: microtask creation logic functions as expected",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with 0 cleanup functions executes in the same microtask as a failing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with 0 cleanup functions executes in the same microtask as a passing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with some cleanup functions executes in the same microtask as a failing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with some cleanup functions executes in the same microtask as a passing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "synchronous test with cleanup",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "synchronous test without cleanup",
+ "properties": {},
+ "status_string": "PASS"
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/uncaught-exception-handle.html b/testing/web-platform/tests/resources/test/tests/functional/uncaught-exception-handle.html
new file mode 100644
index 0000000000..764b0c4055
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/uncaught-exception-handle.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Harness Handling Uncaught Exception</title>
+</head>
+<script src="/resources/testharness.js"></script>
+
+<body>
+<h1>Harness Handling Uncaught Exception</h1>
+<div id="log"></div>
+<script>
+var t = async_test("This should show a harness status of 'Error' and a test status of 'Not Run'");
+throw new Error("Example Error");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Error: Example Error"
+ },
+ "summarized_tests": [
+ {
+ "status_string": "NOTRUN",
+ "name": "This should show a harness status of 'Error' and a test status of 'Not Run'",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/uncaught-exception-ignore.html b/testing/web-platform/tests/resources/test/tests/functional/uncaught-exception-ignore.html
new file mode 100644
index 0000000000..6bd0ddbb0d
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/uncaught-exception-ignore.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Harness Ignoring Uncaught Exception</title>
+</head>
+<script src="/resources/testharness.js"></script>
+
+<body>
+<h1>Harness Ignoring Uncaught Exception</h1>
+<div id="log"></div>
+<script>
+setup({allow_uncaught_exception:true});
+var t = async_test("setup({allow_uncaught_exception:true}) should allow tests to pass even if there is an exception");
+onerror = t.step_func(function() {t.done()});
+throw new Error("Example Error");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "setup({allow_uncaught_exception:true}) should allow tests to pass even if there is an exception",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/worker-dedicated-uncaught-allow.html b/testing/web-platform/tests/resources/test/tests/functional/worker-dedicated-uncaught-allow.html
new file mode 100644
index 0000000000..ba28d4914f
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/worker-dedicated-uncaught-allow.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Dedicated Worker Tests - Allowed Uncaught Exception</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<h1>Dedicated Web Worker Tests - Allowed Uncaught Exception</h1>
+<p>Demonstrates running <tt>testharness</tt> based tests inside a dedicated web worker.
+<p>The test harness is expected to pass despite an uncaught exception in a worker because that worker is configured to allow uncaught exceptions.</p>
+<div id="log"></div>
+
+<script>
+test(function(t) {
+ assert_true("Worker" in self, "Browser should support Workers");
+ },
+ "Browser supports Workers");
+
+fetch_tests_from_worker(new Worker("worker-uncaught-allow.js"));
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Browser supports Workers",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "onerror event is triggered",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/worker-dedicated-uncaught-single.html b/testing/web-platform/tests/resources/test/tests/functional/worker-dedicated-uncaught-single.html
new file mode 100644
index 0000000000..486e067114
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/worker-dedicated-uncaught-single.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Dedicated Worker Tests - Uncaught Exception in Single-Page Test</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<h1>Dedicated Web Worker Tests - Uncaught Exception in Single-Page Test</h1>
+<p>Demonstrates running <tt>testharness</tt> based tests inside a dedicated web worker.
+<p>The test harness is expected to pass despite an uncaught exception in a worker because that worker is a single-page test.</p>
+<div id="log"></div>
+
+<script>
+test(function(t) {
+ assert_true("Worker" in self, "Browser should support Workers");
+ },
+ "Browser supports Workers");
+
+fetch_tests_from_worker(new Worker("worker-uncaught-single.js"));
+
+test(function(t) {
+ assert_false(false, "False is false");
+ },
+ "Test running on main document.");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Browser supports Workers",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test running on main document.",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "FAIL",
+ "name": "worker-uncaught-single",
+ "properties": {},
+ "message": "Error: This failure is expected."
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/worker-dedicated.sub.html b/testing/web-platform/tests/resources/test/tests/functional/worker-dedicated.sub.html
new file mode 100644
index 0000000000..efd703c760
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/worker-dedicated.sub.html
@@ -0,0 +1,88 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Dedicated Worker Tests</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<h1>Dedicated Web Worker Tests</h1>
+<p>Demonstrates running <tt>testharness</tt> based tests inside a dedicated web worker.
+<p>The test harness is expected to fail due to an uncaught exception in one worker.</p>
+<div id="log"></div>
+
+<script>
+test(function(t) {
+ assert_true("Worker" in self, "Browser should support Workers");
+ },
+ "Browser supports Workers");
+
+fetch_tests_from_worker(new Worker("worker.js"));
+
+fetch_tests_from_worker(new Worker("worker-error.js"));
+
+test(function(t) {
+ assert_false(false, "False is false");
+ },
+ "Test running on main document.");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Error: This failure is expected."
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Browser supports Workers",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test running on main document.",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Worker async_test that completes successfully",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Worker test that completes successfully",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "worker test that completes successfully before exception",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "Worker test that doesn't run ('NOT RUN')",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Worker test that fails ('FAIL')",
+ "properties": {},
+ "message": "assert_true: Failing test expected true got false"
+ },
+ {
+ "status_string": "TIMEOUT",
+ "name": "Worker test that times out ('TIMEOUT')",
+ "properties": {},
+ "message": "Test timed out"
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/worker-error.js b/testing/web-platform/tests/resources/test/tests/functional/worker-error.js
new file mode 100644
index 0000000000..7b89602f04
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/worker-error.js
@@ -0,0 +1,8 @@
+importScripts("/resources/testharness.js");
+
+// The following sub-test ensures that the worker is not interpreted as a
+// single-page test. The subsequent uncaught exception should therefore be
+// interpreted as a harness error rather than a single-page test failure.
+test(function() {}, "worker test that completes successfully before exception");
+
+throw new Error("This failure is expected.");
diff --git a/testing/web-platform/tests/resources/test/tests/functional/worker-service.html b/testing/web-platform/tests/resources/test/tests/functional/worker-service.html
new file mode 100644
index 0000000000..2e07746e62
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/worker-service.html
@@ -0,0 +1,115 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Example with a service worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<h1>Service Worker Tests</h1>
+<p>Demonstrates running <tt>testharness</tt> based tests inside a service worker.
+<p>The test harness should time out due to one of the tests inside the worker timing out.
+<p>This test assumes that the browser supports <a href="http://www.w3.org/TR/service-workers/">ServiceWorkers</a>.
+<div id="log"></div>
+
+<script>
+test(
+ function(t) {
+ assert_true("serviceWorker" in navigator,
+ "navigator.serviceWorker exists");
+ },
+ "Browser supports ServiceWorker");
+
+promise_test(
+ function() {
+ // Since the service worker registration could be in an indeterminate
+ // state (due to, for example, a previous test run failing), we start by
+ // unregstering our service worker and then registering it again.
+ var scope = "service-worker-scope";
+ var worker_url = "worker.js";
+
+ return navigator.serviceWorker.register(worker_url, {scope: scope})
+ .then(
+ function(registration) {
+ return registration.unregister();
+ })
+ .then(
+ function() {
+ return navigator.serviceWorker.register(worker_url, {scope: scope});
+ })
+ .then(
+ function(registration) {
+ add_completion_callback(
+ function() {
+ registration.unregister();
+ });
+
+ return new Promise(
+ function(resolve) {
+ registration.addEventListener("updatefound",
+ function() {
+ resolve(registration.installing);
+ });
+ });
+ })
+ .then(
+ function(worker) {
+ fetch_tests_from_worker(worker);
+ });
+ },
+ "Register ServiceWorker");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "TIMEOUT",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Browser supports ServiceWorker",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Register ServiceWorker",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Worker async_test that completes successfully",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Worker test that completes successfully",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "Worker test that doesn't run ('NOT RUN')",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Worker test that fails ('FAIL')",
+ "properties": {},
+ "message": "assert_true: Failing test expected true got false"
+ },
+ {
+ "status_string": "TIMEOUT",
+ "name": "Worker test that times out ('TIMEOUT')",
+ "properties": {},
+ "message": "Test timed out"
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/worker-shared.html b/testing/web-platform/tests/resources/test/tests/functional/worker-shared.html
new file mode 100644
index 0000000000..e26f17dec2
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/worker-shared.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Example with a shared worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<h1>Shared Web Worker Tests</h1>
+<p>Demonstrates running <tt>testharness</tt> based tests inside a shared worker.
+<p>The test harness should time out due to one of the tests in the worker timing out.
+<p>This test assumes that the browser supports <a href="http://www.w3.org/TR/workers/#shared-workers-and-the-sharedworker-interface">shared web workers</a>.
+<div id="log"></div>
+
+<script>
+test(
+ function(t) {
+ assert_true("SharedWorker" in self,
+ "Browser should support SharedWorkers");
+ },
+ "Browser supports SharedWorkers");
+
+fetch_tests_from_worker(new SharedWorker("worker.js",
+ "My shared worker"));
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "TIMEOUT",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Browser supports SharedWorkers",
+ "properties": {},
+ "message": null
+ },
+ {
+ "message": null,
+ "name": "Worker async_test that completes successfully",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "Worker test that completes successfully",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "Worker test that doesn't run ('NOT RUN')",
+ "properties": {},
+ "status_string": "NOTRUN"
+ },
+ {
+ "message": "assert_true: Failing test expected true got false",
+ "name": "Worker test that fails ('FAIL')",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": "Test timed out",
+ "name": "Worker test that times out ('TIMEOUT')",
+ "properties": {},
+ "status_string": "TIMEOUT"
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/worker-uncaught-allow.js b/testing/web-platform/tests/resources/test/tests/functional/worker-uncaught-allow.js
new file mode 100644
index 0000000000..6925d59349
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/worker-uncaught-allow.js
@@ -0,0 +1,19 @@
+importScripts("/resources/testharness.js");
+
+setup({allow_uncaught_exception:true});
+
+async_test(function(t) {
+ onerror = function() {
+ // Further delay the test's completion to ensure that the worker's
+ // `onerror` handler does not influence results in the parent context.
+ setTimeout(function() {
+ t.done();
+ }, 0);
+ };
+
+ setTimeout(function() {
+ throw new Error("This error is expected.");
+ }, 0);
+}, 'onerror event is triggered');
+
+done();
diff --git a/testing/web-platform/tests/resources/test/tests/functional/worker-uncaught-single.js b/testing/web-platform/tests/resources/test/tests/functional/worker-uncaught-single.js
new file mode 100644
index 0000000000..c04542b2f5
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/worker-uncaught-single.js
@@ -0,0 +1,8 @@
+importScripts("/resources/testharness.js");
+
+setup({ single_test: true });
+
+// Because this script enables the `single_test` configuration option, it
+// should be interpreted as a single-page test, and the uncaught exception
+// should be reported as a test failure (harness status: OK).
+throw new Error("This failure is expected.");
diff --git a/testing/web-platform/tests/resources/test/tests/functional/worker.js b/testing/web-platform/tests/resources/test/tests/functional/worker.js
new file mode 100644
index 0000000000..a923bc2d89
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/worker.js
@@ -0,0 +1,34 @@
+importScripts("/resources/testharness.js");
+
+test(
+ function(test) {
+ assert_true(true, "True is true");
+ },
+ "Worker test that completes successfully");
+
+test(
+ function(test) {
+ assert_true(false, "Failing test");
+ },
+ "Worker test that fails ('FAIL')");
+
+async_test(
+ function(test) {
+ assert_true(true, "True is true");
+ },
+ "Worker test that times out ('TIMEOUT')");
+
+async_test("Worker test that doesn't run ('NOT RUN')");
+
+async_test(
+ function(test) {
+ self.setTimeout(
+ function() {
+ test.done();
+ },
+ 0);
+ },
+ "Worker async_test that completes successfully");
+
+// An explicit done() is required for dedicated and shared web workers.
+done();
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlArray/is_json_type.html b/testing/web-platform/tests/resources/test/tests/unit/IdlArray/is_json_type.html
new file mode 100644
index 0000000000..18e83a8e89
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlArray/is_json_type.html
@@ -0,0 +1,192 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>IdlArray.prototype.is_json_type()</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+ "use strict";
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_true(idl.is_json_type(typeFrom("DOMString")));
+ assert_true(idl.is_json_type(typeFrom("ByteString")));
+ assert_true(idl.is_json_type(typeFrom("USVString")));
+ idl.add_untested_idls('enum BarEnum { "a", "b", "c" };');
+ assert_true(idl.is_json_type(typeFrom("BarEnum")));
+ }, 'should return true for all string types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_false(idl.is_json_type(typeFrom("Error")));
+ assert_false(idl.is_json_type(typeFrom("DOMException")));
+ }, 'should return false for all exception types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_false(idl.is_json_type(typeFrom("Int8Array")));
+ assert_false(idl.is_json_type(typeFrom("Int16Array")));
+ assert_false(idl.is_json_type(typeFrom("Int32Array")));
+ assert_false(idl.is_json_type(typeFrom("Uint8Array")));
+ assert_false(idl.is_json_type(typeFrom("Uint16Array")));
+ assert_false(idl.is_json_type(typeFrom("Uint32Array")));
+ assert_false(idl.is_json_type(typeFrom("Uint8ClampedArray")));
+ assert_false(idl.is_json_type(typeFrom("BigInt64Array")));
+ assert_false(idl.is_json_type(typeFrom("BigUint64Array")));
+ assert_false(idl.is_json_type(typeFrom("Float32Array")));
+ assert_false(idl.is_json_type(typeFrom("Float64Array")));
+ assert_false(idl.is_json_type(typeFrom("ArrayBuffer")));
+ assert_false(idl.is_json_type(typeFrom("DataView")));
+ }, 'should return false for all buffer source types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_true(idl.is_json_type(typeFrom("boolean")));
+ }, 'should return true for boolean');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_true(idl.is_json_type(typeFrom("byte")));
+ assert_true(idl.is_json_type(typeFrom("octet")));
+ assert_true(idl.is_json_type(typeFrom("short")));
+ assert_true(idl.is_json_type(typeFrom("unsigned short")));
+ assert_true(idl.is_json_type(typeFrom("long")));
+ assert_true(idl.is_json_type(typeFrom("unsigned long")));
+ assert_true(idl.is_json_type(typeFrom("long long")));
+ assert_true(idl.is_json_type(typeFrom("unsigned long long")));
+ assert_true(idl.is_json_type(typeFrom("float")));
+ assert_true(idl.is_json_type(typeFrom("unrestricted float")));
+ assert_true(idl.is_json_type(typeFrom("double")));
+ assert_true(idl.is_json_type(typeFrom("unrestricted double")));
+ }, 'should return true for all numeric types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_false(idl.is_json_type(typeFrom("Promise<DOMString>")));
+ }, 'should return false for promises');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_false(idl.is_json_type(typeFrom("sequence<DOMException>")));
+ assert_true(idl.is_json_type(typeFrom("sequence<DOMString>")));
+ }, 'should handle sequences according to their inner types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_false(idl.is_json_type(typeFrom("FrozenArray<DOMException>")));
+ assert_true(idl.is_json_type(typeFrom("FrozenArray<DOMString>")));
+ }, 'should handle frozen arrays according to their inner types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_true(idl.is_json_type(typeFrom("record<DOMString, DOMString>")));
+ assert_false(idl.is_json_type(typeFrom("record<DOMString, Error>")));
+ }, 'should handle records according to their inner types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_true(idl.is_json_type(typeFrom("object")));
+ }, 'should return true for object type');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_false(idl.is_json_type(typeFrom("any")));
+ }, 'should return false for any type');
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_untested_idls('dictionary Foo { DOMString foo; }; dictionary Bar : Foo { DOMString bar; };');
+ assert_true(idl.is_json_type(typeFrom("Foo")));
+ assert_true(idl.is_json_type(typeFrom("Bar")));
+ }, 'should return true for dictionaries whose members are all JSON types');
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_untested_idls('dictionary Foo { };');
+ assert_true(idl.is_json_type(typeFrom("Foo")));
+ }, 'should return true for dictionaries which have no members');
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_untested_idls('dictionary FooBar { DOMString a; Error b; }; dictionary Baz : FooBar {};');
+ assert_false(idl.is_json_type(typeFrom("FooBar")));
+ assert_false(idl.is_json_type(typeFrom("Baz")));
+ }, 'should return false for dictionaries whose members are not all JSON types');
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_untested_idls('interface Foo { DOMString toJSON(); };');
+ assert_true(idl.is_json_type(typeFrom("Foo")));
+ }, 'should return true for interfaces which declare a toJSON operation');
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_untested_idls('interface Foo { DOMString toJSON(); }; interface Bar : Foo { };');
+ assert_true(idl.is_json_type(typeFrom("Bar")));
+ }, 'should return true for interfaces which inherit from an interface which declares a toJSON operation');
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_untested_idls('interface Foo { }; interface mixin Bar { DOMString toJSON(); }; Foo includes Bar;');
+ idl.merge_mixins();
+ assert_true(idl.is_json_type(typeFrom("Foo")));
+ }, 'should return true for interfaces which mixin an interface which declare a toJSON operation');
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_untested_idls('interface Foo { };');
+ assert_false(idl.is_json_type(typeFrom("Foo")));
+ }, 'should return false for interfaces which do not declare a toJSON operation');
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_untested_idls('interface Foo { object toJSON(); };');
+ assert_true(idl.is_json_type(typeFrom("(Foo or DOMString)")));
+ }, 'should return true for union types whose member types are JSON types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_false(idl.is_json_type(typeFrom("(DataView or DOMString)")));
+ }, 'should return false for union types whose member types are not all JSON types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_true(idl.is_json_type(typeFrom("DOMString?")));
+ assert_false(idl.is_json_type(typeFrom("DataView?")));
+ }, 'should consider the inner types of nullable types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_true(idl.is_json_type(typeFrom("[XAttr] long")));
+ assert_false(idl.is_json_type(typeFrom("[XAttr] DataView")));
+ }, 'should consider the inner types of annotated types.');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_throws_js(Error, _ => idl.is_json_type(typeFrom("Foo")));
+ }, "should throw if it references a dictionary, enum or interface which wasn't added to the IdlArray");
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_untested_idls('interface Foo : Bar { };');
+ assert_throws_js(Error, _ => idl.is_json_type(typeFrom("Foo")));
+ }, "should throw for interfaces which inherit from another interface which wasn't added to the IdlArray");
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_true(idl.is_json_type(typedefFrom("typedef double DOMHighResTimeStamp;").idlType));
+ }, 'should return true for typedefs whose source type is a JSON type');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_false(idl.is_json_type(typedefFrom("typedef DataView DOMHighResTimeStamp;").idlType));
+ }, 'should return false for typedefs whose source type is not a JSON type');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlDictionary/get_reverse_inheritance_stack.html b/testing/web-platform/tests/resources/test/tests/unit/IdlDictionary/get_reverse_inheritance_stack.html
new file mode 100644
index 0000000000..418bcdec92
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlDictionary/get_reverse_inheritance_stack.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>IdlDictionary.prototype.get_reverse_inheritance_stack()</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+ "use strict";
+ test(function() {
+ var stack = dictionaryFrom('dictionary A { };').get_reverse_inheritance_stack();
+ assert_array_equals(stack.map(d => d.name), ["A"]);
+ }, 'should return an array that includes itself.');
+
+ test(function() {
+ var d = dictionaryFrom('dictionary A : B { };');
+ assert_throws_js(Error, _ => d.get_reverse_inheritance_stack());
+ }, "should throw for dictionaries which inherit from another dictionary which wasn't added to the IdlArray");
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_idls('dictionary A : B { };');
+ idl.add_untested_idls('dictionary B : C { }; dictionary C { };');
+ var A = idl.members["A"];
+ assert_array_equals(A.get_reverse_inheritance_stack().map(d => d.name), ["C", "B", "A"]);
+ }, 'should return an array of dictionaries in order of inheritance, starting with the base dictionary');
+
+ test(function () {
+ let i = new IdlArray();
+ i.add_untested_idls('dictionary A : B {};');
+ i.assert_throws(new IdlHarnessError('A inherits B, but B is undefined.'), i => i.test());
+ }, 'A : B with B undeclared should throw IdlHarnessError');
+
+ test(function () {
+ let i = new IdlArray();
+ i.add_untested_idls('dictionary A : B {};');
+ i.add_untested_idls('interface B {};');
+ i.assert_throws(new IdlHarnessError('A inherits B, but A is not an interface.'), i => i.test());
+ }, 'dictionary A : B with B interface should throw IdlHarnessError');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlDictionary/test_partial_dictionary.html b/testing/web-platform/tests/resources/test/tests/unit/IdlDictionary/test_partial_dictionary.html
new file mode 100644
index 0000000000..d6137f6895
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlDictionary/test_partial_dictionary.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>idlharness: partial dictionaries</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+ <script src="../../../idl-helper.js"></script>
+</head>
+
+<body>
+<pre id='idl'>
+dictionary A {};
+partial dictionary A {
+ boolean B;
+};
+partial dictionary A {
+ boolean C;
+};
+</pre>
+
+<script>
+'use strict';
+
+test(() => {
+ let idlArray = new IdlArray();
+ idlArray.add_idls(document.getElementById('idl').textContent);
+ idlArray.test();
+
+ let members = idlArray.members["A"].members.map(m => m.name);
+ assert_array_equals(members, ["B", "C"], 'A should contain B, C');
+}, 'Partial dictionaries');
+</script>
+
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/constructors.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/constructors.html
new file mode 100644
index 0000000000..e9ee3f8680
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/constructors.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<title>IdlInterface.prototype.constructors()</title>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+"use strict";
+// [Constructor] extended attribute should not be supported:
+test(function() {
+ var i = interfaceFrom('[Constructor] interface A { };');
+ assert_equals(i.constructors().length, 0);
+}, 'Interface with Constructor extended attribute.');
+
+test(function() {
+ var i = interfaceFrom('interface A { constructor(); };');
+ assert_equals(i.constructors().length, 1);
+}, 'Interface with constructor method');
+
+test(function() {
+ var i = interfaceFrom('interface A { constructor(); constructor(any value); };');
+ assert_equals(i.constructors().length, 2);
+}, 'Interface with constructor overloads');
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/default_to_json_operation.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/default_to_json_operation.html
new file mode 100644
index 0000000000..5ade7d0d28
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/default_to_json_operation.html
@@ -0,0 +1,114 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>IdlDictionary.prototype.default_to_json_operation()</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+ "use strict";
+ test(function() {
+ var map = interfaceFrom('interface A { [Default] object toJSON(); };').default_to_json_operation();
+ assert_equals(map.size, 0);
+ }, 'should return an empty map when there are no attributes');
+
+ test(function() {
+ var r = interfaceFrom('interface A { };').default_to_json_operation();
+ assert_equals(r, null);
+ }, 'should return null when there is no toJSON method');
+
+ test(function() {
+ var r = interfaceFrom('interface A { DOMString toJSON(); };').default_to_json_operation();
+ assert_equals(r, null);
+ }, 'should return null when there is a toJSON method but it does not have the [Default] extended attribute');
+
+ test(function() {
+ var context = new IdlArray();
+ context.add_idls("interface A : B { DOMString toJSON(); };");
+ context.add_idls("interface B { [Default] object toJSON(); };");
+ var r = context.members.A.default_to_json_operation();
+ assert_equals(r, null);
+ }, 'should return null when there is a toJSON method but it does not have the [Default] extended attribute even if this extended attribute exists on inherited interfaces');
+
+ test(function() {
+ var map = interfaceFrom('interface A { [Default] object toJSON(); static attribute DOMString foo; };').default_to_json_operation();
+ assert_equals(map.size, 0);
+ }, 'should not include static attributes');
+
+ test(function() {
+ var map = interfaceFrom('interface A { [Default] object toJSON(); attribute Promise<DOMString> bar; };').default_to_json_operation();
+ assert_equals(map.size, 0);
+ }, 'should not include attributes which are not JSON types');
+
+ test(function() {
+ var map = interfaceFrom('interface A { [Default] object toJSON(); DOMString bar(); };').default_to_json_operation();
+ assert_equals(map.size, 0);
+ }, 'should not include operations');
+
+ test(function() {
+ var map = interfaceFrom('interface A { [Default] object toJSON(); attribute DOMString bar; };').default_to_json_operation();
+ assert_equals(map.size, 1);
+ assert_true(map.has("bar"));
+ assert_equals(map.get("bar").idlType, "DOMString");
+ }, 'should return a map whose key/value pair represent the identifier and IDL type of valid attributes');
+
+ test(function() {
+ var context = new IdlArray();
+ context.add_idls("interface A : B { [Default] object toJSON(); attribute DOMString a; };");
+ context.add_idls("interface B { [Default] object toJSON(); attribute long b; };");
+ var map = context.members.A.default_to_json_operation();
+ assert_array_equals([...map.keys()], ["b", "a"]);
+ assert_array_equals([...map.values()].map(v => v.idlType), ["long", "DOMString"]);
+ }, 'should return a properly ordered map that contains IDL types of valid attributes for inherited interfaces');
+
+ test(function() {
+ var context = new IdlArray();
+ context.add_idls("interface A : B { attribute DOMString a; };");
+ context.add_idls("interface B { [Default] object toJSON(); attribute long b; };");
+ var map = context.members.A.default_to_json_operation();
+ assert_equals(map.size, 1);
+ assert_true(map.has("b"));
+ assert_equals(map.get("b").idlType, "long");
+ assert_array_equals([...map.keys()], ["b"]);
+ }, 'should not include attributes of the current interface when the [Default] toJSON method in inherited');
+
+ test(function() {
+ var context = new IdlArray();
+ context.add_idls("interface A : B { [Default] object toJSON(); };");
+ context.add_idls("interface B : C { [Default] object toJSON(); attribute DOMString foo; };");
+ context.add_idls("interface C { [Default] object toJSON(); attribute long foo; };");
+ var map = context.members.A.default_to_json_operation();
+ assert_equals(map.size, 1);
+ assert_true(map.has("foo"));
+ assert_equals(map.get("foo").idlType, "DOMString");
+ }, 'attributes declared further away in the inheritance hierarchy should be masked by attributes declared closer');
+
+ test(function() {
+ var context = new IdlArray();
+ context.add_idls("interface A { [Default] object toJSON(); attribute DOMString a; };");
+ context.add_idls("interface B : A { attribute any b; };");
+ context.add_idls("interface C : B { [Default] object toJSON(); attribute long c; };");
+ var map = context.members.C.default_to_json_operation();
+ assert_array_equals([...map.keys()], ["a", "c"]);
+ assert_array_equals([...map.values()].map(v => v.idlType), ["DOMString", "long"]);
+ }, 'should return an ordered map that ignores attributes of inherited interfaces which do not declare a [Default] toJSON operation.');
+
+ test(function() {
+ var context = new IdlArray();
+ context.add_idls("interface D { attribute DOMString d; };");
+ context.add_idls("interface mixin M { [Default] object toJSON(); attribute long m; };");
+ context.add_idls("D includes M;");
+ context.merge_mixins();
+ var map = context.members.D.default_to_json_operation();
+ assert_array_equals([...map.keys()], ["d", "m"]);
+ assert_array_equals([...map.values()].map(v => v.idlType), ["DOMString", "long"]);
+ }, 'should return a properly ordered map that accounts for mixed-in interfaces which declare a [Default] toJSON operation.');
+</script>
+</body>
+</html>
+
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/do_member_unscopable_asserts.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/do_member_unscopable_asserts.html
new file mode 100644
index 0000000000..90142efe6b
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/do_member_unscopable_asserts.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>IdlDictionary.prototype.do_member_unscopable_asserts()</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+ 'use strict';
+ function mock_interface_A(unscopables) {
+ self.A = function A() {};
+ A.prototype[Symbol.unscopables] = unscopables;
+ }
+
+ test(function() {
+ const i = interfaceFrom('interface A { [Unscopable] attribute any x; };');
+ const member = i.members[0];
+ assert_true(member.isUnscopable);
+ mock_interface_A({ x: true });
+ i.do_member_unscopable_asserts(member);
+ }, 'should not throw for [Unscopable] with property in @@unscopables');
+
+ test(function() {
+ const i = interfaceFrom('interface A { [Unscopable] attribute any x; };');
+ const member = i.members[0];
+ assert_true(member.isUnscopable);
+ mock_interface_A({});
+ // assert_throws_* can't be used because they rethrow AssertionErrors.
+ try {
+ i.do_member_unscopable_asserts(member);
+ } catch(e) {
+ assert_true(e.message.includes('Symbol.unscopables'));
+ return;
+ }
+ assert_unreached('did not throw');
+ }, 'should throw for [Unscopable] with property missing from @@unscopables');
+
+ // This test checks that for attributes/methods which aren't [Unscopable]
+ // in the IDL, we don't assert that @@unscopables is missing the property.
+ // This could miss implementation bugs, but [Unscopable] is so rarely used
+ // that it's fairly unlikely to ever happen.
+ test(function() {
+ const i = interfaceFrom('interface A { attribute any x; };');
+ const member = i.members[0];
+ assert_false(member.isUnscopable);
+ mock_interface_A({ x: true });
+ i.do_member_unscopable_asserts(member);
+ }, 'should not throw if [Unscopable] is used but property is in @@unscopables');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_interface_object.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_interface_object.html
new file mode 100644
index 0000000000..a3d901a752
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_interface_object.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<title>IdlInterface.prototype.get_interface_object()</title>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+"use strict";
+test(function() {
+ window.A = {};
+ var i = interfaceFrom('interface A { };');
+ assert_equals(i.get_interface_object(), window.A);
+}, 'Interface does not have LegacyNamespace.');
+
+test(function() {
+ window.Foo = { A: {} };
+ var i = interfaceFrom('[LegacyNamespace=Foo] interface A { }; namespace Foo { };');
+ assert_equals(i.get_interface_object(), window.Foo.A);
+}, 'Interface has LegacyNamespace');
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_interface_object_owner.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_interface_object_owner.html
new file mode 100644
index 0000000000..51ab2067bc
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_interface_object_owner.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<title>IdlInterface.prototype.get_interface_object_owner()</title>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+"use strict";
+test(function() {
+ var i = interfaceFrom('interface A { };');
+ assert_equals(i.get_interface_object_owner(), window);
+}, 'Interface does not have LegacyNamespace.');
+
+test(function() {
+ window.Foo = {};
+ var i = interfaceFrom('[LegacyNamespace=Foo] interface A { }; namespace Foo { };');
+ assert_equals(i.get_interface_object_owner(), window.Foo);
+}, 'Interface has LegacyNamespace');
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_legacy_namespace.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_legacy_namespace.html
new file mode 100644
index 0000000000..e2d42bb09e
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_legacy_namespace.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<title>IdlInterface.prototype.get_legacy_namespace()</title>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+"use strict";
+test(function() {
+ var i = interfaceFrom('interface A { };');
+ assert_equals(i.get_legacy_namespace(), undefined);
+}, 'Interface does not have LegacyNamespace.');
+
+test(function() {
+ var i = interfaceFrom('[LegacyNamespace=Foo] interface A { }; namespace Foo { };');
+ assert_equals(i.get_legacy_namespace(), "Foo");
+}, 'Interface has LegacyNamespace');
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_qualified_name.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_qualified_name.html
new file mode 100644
index 0000000000..677a31b5e7
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_qualified_name.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<title>IdlInterface.prototype.get_qualified_name()</title>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+"use strict";
+test(function() {
+ var i = interfaceFrom('interface A { };');
+ assert_equals(i.get_qualified_name(), "A");
+}, 'Interface does not have LegacyNamespace.');
+
+test(function() {
+ var i = interfaceFrom('[LegacyNamespace=Foo] interface A { }; namespace Foo { };');
+ assert_equals(i.get_qualified_name(), "Foo.A");
+}, 'Interface has LegacyNamespace');
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_reverse_inheritance_stack.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_reverse_inheritance_stack.html
new file mode 100644
index 0000000000..0c066baabb
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_reverse_inheritance_stack.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>IdlInterface.prototype.get_reverse_inheritance_stack()</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+ "use strict";
+ test(function() {
+ var stack = interfaceFrom('interface A { };').get_reverse_inheritance_stack();
+ assert_array_equals(stack.map(i => i.name), ["A"]);
+ }, 'should return an array that includes itself.');
+
+ test(function() {
+ var i = interfaceFrom('interface A : B { };');
+ assert_throws_js(Error, _ => i.get_reverse_inheritance_stack());
+ }, "should throw for interfaces which inherit from another interface which wasn't added to the IdlArray");
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_idls('interface A : B { };');
+ idl.add_untested_idls('interface B : C { }; interface C { };');
+ var A = idl.members["A"];
+ assert_array_equals(A.get_reverse_inheritance_stack().map(i => i.name), ["C", "B", "A"]);
+ }, 'should return an array of interfaces in order of inheritance, starting with the base interface');
+
+ test(function () {
+ var idl = new IdlArray();
+ idl.add_untested_idls('interface A : B { };');
+ idl.add_untested_idls('interface B : A { };');
+ idl.assert_throws('A has a circular dependency: A,B,A', i => i.test());
+ }, 'should throw when inheritance is circular');
+
+ test(function () {
+ var idl = new IdlArray();
+ idl.add_untested_idls('interface A : B { };');
+ idl.assert_throws(
+ 'Duplicate identifier A',
+ i => i.add_untested_idls('interface A : C { };'));
+ }, 'should throw when multiple inheritances defined');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/has_default_to_json_regular_operation.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/has_default_to_json_regular_operation.html
new file mode 100644
index 0000000000..b47262b72b
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/has_default_to_json_regular_operation.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>IdlInterface.prototype.has_default_to_json_regular_operation()</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+ "use strict";
+ test(function() {
+ var i = interfaceFrom('interface A { };');
+ assert_false(i.has_default_to_json_regular_operation());
+ }, 'should return false when the interface declares no toJSON operation.');
+
+ test(function() {
+ var i = interfaceFrom('interface A { static object toJSON(); };');
+ assert_false(i.has_default_to_json_regular_operation());
+ }, 'should return false when the interface declares a static toJSON operation.');
+
+ test(function() {
+ var i = interfaceFrom('interface A { object toJSON(); };');
+ assert_false(i.has_default_to_json_regular_operation());
+ }, 'should return false when the interface declares a regular toJSON operation with no extended attribute.');
+
+ test(function() {
+ var i = interfaceFrom('interface A { [x] object toJSON(); };');
+ assert_false(i.has_default_to_json_regular_operation());
+ }, 'should return false when the interface declares a regular toJSON operation with another extented attribute.');
+
+ test(function() {
+ var i = interfaceFrom('interface A { [Default] object toJSON(); };');
+ assert_true(i.has_default_to_json_regular_operation());
+ }, 'should return true when the interface declares a regular toJSON operation with the [Default] extented attribute.');
+
+ test(function() {
+ var i = interfaceFrom('interface A { [Attr, AnotherAttr, Default] object toJSON(); };');
+ assert_true(i.has_default_to_json_regular_operation());
+ }, 'should return true when the interface declares a regular toJSON operation with multiple extended attributes, including [Default].');
+</script>
+</body>
+</html>
+
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/has_to_json_regular_operation.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/has_to_json_regular_operation.html
new file mode 100644
index 0000000000..a1a641bd97
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/has_to_json_regular_operation.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>IdlInterface.prototype.has_to_json_regular_operation()</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+ "use strict";
+ test(function() {
+ var i = interfaceFrom('interface A { };');
+ assert_false(i.has_to_json_regular_operation());
+ }, 'should return false when the interface declares no toJSON operation.');
+
+ test(function() {
+ var i = interfaceFrom('interface A { static object toJSON(); };');
+ assert_false(i.has_to_json_regular_operation());
+ }, 'should return false when the interface declares a static toJSON operation.');
+
+ test(function() {
+ var i = interfaceFrom('interface A { object toJSON(); };');
+ assert_true(i.has_to_json_regular_operation());
+ }, 'should return true when the interface declares a regular toJSON operation.');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/should_have_interface_object.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/should_have_interface_object.html
new file mode 100644
index 0000000000..3ce945751d
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/should_have_interface_object.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<title>IdlInterface.prototype.should_have_interface_object()</title>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+"use strict";
+test(function() {
+ var i = interfaceFrom("callback interface A { const unsigned short B = 0; };");
+ assert_true(i.should_have_interface_object());
+}, "callback interface with a constant");
+
+test(function() {
+ var i = interfaceFrom("callback interface A { undefined b(); sequence<any> c(); };");
+ assert_false(i.should_have_interface_object());
+}, "callback interface without a constant");
+
+test(function() {
+ var i = interfaceFrom("[LegacyNoInterfaceObject] interface A { };");
+ assert_false(i.should_have_interface_object());
+}, "non-callback interface with [LegacyNoInterfaceObject]");
+
+test(function() {
+ var i = interfaceFrom("interface A { };");
+ assert_true(i.should_have_interface_object());
+}, "non-callback interface without [LegacyNoInterfaceObject]");
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/test_primary_interface_of_undefined.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/test_primary_interface_of_undefined.html
new file mode 100644
index 0000000000..0031558ad4
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/test_primary_interface_of_undefined.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <title>idlharness test_primary_interface_of_undefined</title>
+</head>
+
+<body>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+ <script>
+ 'use strict';
+ test(function () {
+ let i = new IdlArray();
+ i.add_untested_idls('interface A : B {};');
+ i.assert_throws(new IdlHarnessError('A inherits B, but B is undefined.'), i => i.test());
+ }, 'A : B with B undeclared should throw IdlHarnessError');
+
+ test(function () {
+ let i = new IdlArray();
+ i.add_untested_idls('interface A : B {};');
+ i.add_untested_idls('dictionary B {};');
+ i.assert_throws(new IdlHarnessError('A inherits B, but B is not an interface.'), i => i.test());
+ }, 'interface A : B with B dictionary should throw IdlHarnessError');
+ </script>
+</body>
+
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterfaceMember/is_to_json_regular_operation.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterfaceMember/is_to_json_regular_operation.html
new file mode 100644
index 0000000000..b3f402dd08
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterfaceMember/is_to_json_regular_operation.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>IdlInterfaceMember.prototype.is_to_json_regular_operation()</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+ "use strict";
+ test(function() {
+ var m = memberFrom("readonly attribute DOMString foo");
+ assert_false(m.is_to_json_regular_operation());
+ }, 'should return false when member is an attribute.');
+
+ test(function() {
+ var m = memberFrom("static undefined foo()");
+ assert_false(m.is_to_json_regular_operation());
+ }, 'should return false when member is a static operation.');
+
+ test(function() {
+ var m = memberFrom("static object toJSON()");
+ assert_false(m.is_to_json_regular_operation());
+ }, 'should return false when member is a static toJSON operation.');
+
+ test(function() {
+ var m = memberFrom("object toJSON()");
+ assert_true(m.is_to_json_regular_operation());
+ }, 'should return true when member is a regular toJSON operation.');
+
+ test(function() {
+ var m = memberFrom("[Foo] object toJSON()");
+ assert_true(m.is_to_json_regular_operation());
+ }, 'should return true when member is a regular toJSON operation with extensible attributes.');
+</script>
+</body>
+</html>
+
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterfaceMember/toString.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterfaceMember/toString.html
new file mode 100644
index 0000000000..054dbb1ccb
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterfaceMember/toString.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>IdlInterfaceMember.prototype.toString()</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+"use strict";
+const tests = [
+ ["long x", "long"],
+ ["long? x", "long?"],
+ ["Promise<long> x", "Promise<long>"],
+ ["Promise<long?> x", "Promise<long?>"],
+ ["sequence<long> x", "sequence<long>"],
+ ["(long or DOMString) x", "(long or DOMString)"],
+ ["long x, boolean y", "long, boolean"],
+ ["long x, optional boolean y", "long, optional boolean"],
+ ["long... args", "long..."],
+ ["sequence<long>... args", "sequence<long>..."],
+ ["(long or DOMString)... args", "(long or DOMString)..."],
+];
+for (const [input, output] of tests) {
+ test(function() {
+ var m = memberFrom(`undefined foo(${input})`);
+ assert_equals(m.toString(), `foo(${output})`);
+ }, `toString for ${input}`);
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/assert_implements.html b/testing/web-platform/tests/resources/test/tests/unit/assert_implements.html
new file mode 100644
index 0000000000..6e35f38502
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/assert_implements.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/test/tests/unit/helpers.js"></script>
+<title>assert_implements unittests</title>
+<script>
+'use strict';
+
+test(() => {
+ // All values in JS that are not falsy are truthy, so we just check some
+ // common cases here.
+ assert_implements(true, 'true is a truthy value');
+ assert_implements(5, 'positive integeter is a truthy value');
+ assert_implements(-5, 'negative integeter is a truthy value');
+ assert_implements('foo', 'non-empty string is a truthy value');
+}, 'truthy values');
+
+test_failure(() => {
+ assert_implements(false);
+}, 'false is a falsy value');
+
+test_failure(() => {
+ assert_implements(0);
+}, '0 is a falsy value');
+
+test_failure(() => {
+ assert_implements('');
+}, 'empty string is a falsy value');
+
+test_failure(() => {
+ assert_implements(null);
+}, 'null is a falsy value');
+
+test_failure(() => {
+ assert_implements(undefined);
+}, 'undefined is a falsy value');
+
+test_failure(() => {
+ assert_implements(NaN);
+}, 'NaN is a falsy value');
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/assert_implements_optional.html b/testing/web-platform/tests/resources/test/tests/unit/assert_implements_optional.html
new file mode 100644
index 0000000000..4f23e203c5
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/assert_implements_optional.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/test/tests/unit/helpers.js"></script>
+<title>assert_implements_optional unittests</title>
+<script>
+'use strict';
+
+test(() => {
+ // All values in JS that are not falsy are truthy, so we just check some
+ // common cases here.
+ assert_implements_optional(true, 'true is a truthy value');
+ assert_implements_optional(5, 'positive integeter is a truthy value');
+ assert_implements_optional(-5, 'negative integeter is a truthy value');
+ assert_implements_optional('foo', 'non-empty string is a truthy value');
+}, 'truthy values');
+
+test_failure(() => {
+ assert_implements_optional(false);
+}, 'false is a falsy value');
+
+test_failure(() => {
+ assert_implements_optional(0);
+}, '0 is a falsy value');
+
+test_failure(() => {
+ assert_implements_optional('');
+}, 'empty string is a falsy value');
+
+test_failure(() => {
+ assert_implements_optional(null);
+}, 'null is a falsy value');
+
+test_failure(() => {
+ assert_implements_optional(undefined);
+}, 'undefined is a falsy value');
+
+test_failure(() => {
+ assert_implements_optional(NaN);
+}, 'NaN is a falsy value');
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/assert_object_equals.html b/testing/web-platform/tests/resources/test/tests/unit/assert_object_equals.html
new file mode 100644
index 0000000000..313d77b977
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/assert_object_equals.html
@@ -0,0 +1,152 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/test/tests/unit/helpers.js"></script>
+<title>Assertion functions</title>
+<script>
+'use strict';
+
+test(function() {
+ assert_object_equals({}, {});
+}, 'empty objects');
+
+test(function() {
+ var actual = {};
+ var expected = {};
+ actual.a = actual;
+ expected.a = actual;
+
+ assert_object_equals(actual, expected);
+}, 'tolerates cycles in actual value');
+
+test(function() {
+ var actual = {};
+ var expected = {};
+ actual.a = expected;
+ expected.a = expected;
+
+ assert_object_equals(actual, expected);
+}, 'tolerates cycles in expected value');
+
+test(function() {
+ assert_object_equals({2: 99, 0: 23, 1: 45}, [23, 45, 99]);
+}, 'recognizes equivalence of actual object and expected array');
+
+test(function() {
+ assert_object_equals([23, 45, 99], {2: 99, 0: 23, 1: 45});
+}, 'recognizes equivalence of actual array and expected object');
+
+test(function() {
+ var actual = {};
+ var expected = {};
+ Object.defineProperty(actual, 'a', { value: 1, enumerable: false });
+
+ assert_not_equals(actual.a, expected.a);
+ assert_object_equals(actual, expected);
+}, 'non-enumerable properties in actual value ignored');
+
+test(function() {
+ var actual = {};
+ var expected = {};
+ Object.defineProperty(expected, 'a', { value: 1, enumerable: false });
+
+ assert_not_equals(actual.a, expected.a);
+ assert_object_equals(actual, expected);
+}, 'non-enumerable properties in expected value ignored');
+
+test(function() {
+ assert_object_equals({c: 3, a: 1, b: 2}, {a: 1, b: 2, c: 3});
+}, 'equivalent objects - "flat" object');
+
+test(function() {
+ assert_object_equals(
+ {c: {e: 5, d: 4}, b: 2, a: 1},
+ {a: 1, b: 2, c: {d: 4, e: 5}}
+ );
+}, 'equivalent objects - nested object');
+
+test(function() {
+ assert_object_equals(
+ {c: [4, 5], b: 2, a: 1},
+ {a: 1, b: 2, c: [4, 5]}
+ );
+}, 'equivalent objects - nested array');
+
+test(function() {
+ assert_object_equals({a: NaN}, {a: NaN});
+}, 'equivalent objects - NaN value');
+
+test(function() {
+ assert_object_equals({a: -0}, {a: -0});
+}, 'equivalent objects - negative zero value');
+
+test_failure(function() {
+ assert_object_equals(undefined, {});
+}, 'invalid actual value: undefined');
+
+test_failure(function() {
+ assert_object_equals(null, {});
+}, 'invalid actual value: null');
+
+test_failure(function() {
+ assert_object_equals(34, {});
+}, 'invalid actual value: number');
+
+test_failure(function() {
+ assert_object_equals({c: 3, a: 1, b: 2}, {a: 1, b: 1, c: 3});
+}, 'unequal property value - "flat" object');
+
+test_failure(function() {
+ var actual = Object.create({a: undefined});
+ var expected = {};
+
+ assert_object_equals(actual, expected);
+}, 'non-own properties in actual value verified');
+
+test_failure(function() {
+ var actual = {};
+ var expected = Object.create({a: undefined});
+
+ assert_object_equals(actual, expected);
+}, 'non-own properties in expected value verified');
+
+test_failure(function() {
+ assert_object_equals(
+ {a: 1, b: 2, c: {d: 5, e: 5, f: 6}},
+ {a: 1, b: 2, c: {d: 5, e: 7, f: 6}}
+ );
+}, 'unequal property value - nested object');
+
+test_failure(function() {
+ assert_object_equals(
+ {a: 1, b: 2, c: [4, 5, 6]},
+ {a: 1, b: 2, c: [4, 7, 6]}
+ );
+}, 'unequal property value - nested array');
+
+test_failure(function() {
+ assert_object_equals({a: NaN}, {a: 0});
+}, 'equivalent objects - NaN actual value');
+
+test_failure(function() {
+ assert_object_equals({a: 0}, {a: NaN});
+}, 'equivalent objects - NaN expected value');
+
+test_failure(function() {
+ assert_object_equals({a: -0}, {a: 0});
+}, 'equivalent objects - negative zero actual value');
+
+test_failure(function() {
+ assert_object_equals({a: 0}, {a: -0});
+}, 'equivalent objects - negative zero expected value');
+
+test_failure(function() {
+ assert_object_equals({a: 1}, {});
+}, 'actual contains additional property');
+
+test_failure(function() {
+ assert_object_equals({}, {a: 1});
+}, 'expected contains additional property');
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/async-test-return-restrictions.html b/testing/web-platform/tests/resources/test/tests/unit/async-test-return-restrictions.html
new file mode 100644
index 0000000000..0fde2e2422
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/async-test-return-restrictions.html
@@ -0,0 +1,135 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="/resources/testharness.js"></script>
+ <title>Restrictions on return value from `async_test`</title>
+</head>
+<body>
+<script>
+function makeTest(...bodies) {
+ const closeScript = '<' + '/script>';
+ let src = `
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Document title</title>
+<script src="/resources/testharness.js?${Math.random()}">${closeScript}
+</head>
+
+<body>
+<div id="log"></div>`;
+ bodies.forEach((body) => {
+ src += '<script>(' + body + ')();' + closeScript;
+ });
+
+ const iframe = document.createElement('iframe');
+
+ document.body.appendChild(iframe);
+ iframe.contentDocument.write(src);
+
+ return new Promise((resolve) => {
+ window.addEventListener('message', function onMessage(e) {
+ if (e.source !== iframe.contentWindow) {
+ return;
+ }
+ if (!e.data || e.data.type !=='complete') {
+ return;
+ }
+ window.removeEventListener('message', onMessage);
+ resolve(e.data);
+ });
+
+ iframe.contentDocument.close();
+ }).then(({ tests, status }) => {
+ const summary = {
+ harness: {
+ status: getEnumProp(status, status.status),
+ message: status.message
+ },
+ tests: {}
+ };
+
+ tests.forEach((test) => {
+ summary.tests[test.name] = getEnumProp(test, test.status);
+ });
+
+ return summary;
+ });
+}
+
+function getEnumProp(object, value) {
+ for (let property in object) {
+ if (!/^[A-Z]+$/.test(property)) {
+ continue;
+ }
+
+ if (object[property] === value) {
+ return property;
+ }
+ }
+}
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ async_test((t) => {t.done(); return undefined;}, 'before');
+ async_test((t) => {t.done(); return null;}, 'null');
+ async_test((t) => {t.done(); return undefined;}, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'ERROR');
+ assert_equals(
+ harness.message,
+ 'Test named "null" passed a function to `async_test` that returned a value.'
+ );
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.null, 'PASS');
+ // This test did not get the chance to start.
+ assert_equals(tests.after, undefined);
+ });
+}, 'test returning `null`');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ async_test((t) => {t.done(); return undefined;}, 'before');
+ async_test((t) => {t.done(); return {};}, 'object');
+ async_test((t) => {t.done(); return undefined;}, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'ERROR');
+ assert_equals(
+ harness.message,
+ 'Test named "object" passed a function to `async_test` that returned a value.'
+ );
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.object, 'PASS');
+ // This test did not get the chance to start.
+ assert_equals(tests.after, undefined);
+ });
+}, 'test returning an ordinary object');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ async_test((t) => {t.done(); return undefined;}, 'before');
+ async_test((t) => {t.done(); return Promise.resolve(5);}, 'thenable');
+ async_test((t) => {t.done(); return undefined;}, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'ERROR');
+ assert_equals(
+ harness.message,
+ 'Test named "thenable" passed a function to `async_test` that returned a value. ' +
+ 'Consider using `promise_test` instead when using Promises or async/await.'
+ );
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.thenable, 'PASS');
+ // This test did not get a chance to start.
+ assert_equals(tests.after, undefined);
+ });
+}, 'test returning a thenable object');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/basic.html b/testing/web-platform/tests/resources/test/tests/unit/basic.html
new file mode 100644
index 0000000000..d52082f2e0
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/basic.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>idlharness basic</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script>
+ "use strict";
+ test(function() {
+ assert_true("IdlArray" in window);
+ }, 'IdlArray constructor should be a global object');
+ test(function() {
+ assert_true(new IdlArray() instanceof IdlArray);
+ }, 'IdlArray constructor should be constructible');
+ test(function() {
+ assert_true("WebIDL2" in window);
+ }, 'WebIDL2 namespace should be a global object');
+ test(function() {
+ assert_equals(typeof WebIDL2.parse, "function");
+ }, 'WebIDL2 namespace should have a parse method');
+ test(function() {
+ try {
+ WebIDL2.parse("I'm a syntax error");
+ throw new Error("Web IDL didn't throw");
+ } catch (e) {
+ assert_equals(e.name, "WebIDLParseError");
+ }
+ }, 'WebIDL2 parse method should bail on incorrect WebIDL');
+ test(function() {
+ assert_equals(typeof WebIDL2.parse("interface Foo {};"), "object");
+ }, 'WebIDL2 parse method should produce an AST for correct WebIDL');
+ test(function () {
+ try {
+ let i = new IdlArray();
+ i.add_untested_idls(`interface C {};`);
+ i.assert_throws('Anything', i => i.test());
+ } catch (e) {
+ assert_true(e instanceof IdlHarnessError);
+ }
+ }, `assert_throws should throw if no IdlHarnessError thrown`);
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/exceptional-cases-timeouts.html b/testing/web-platform/tests/resources/test/tests/unit/exceptional-cases-timeouts.html
new file mode 100644
index 0000000000..760ac7154f
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/exceptional-cases-timeouts.html
@@ -0,0 +1,120 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <script src="/resources/testharness.js"></script>
+ <title>Exceptional cases - timeouts</title>
+</head>
+<body>
+<p>
+ The tests in this file are executed in parallel to avoid exceeding the "long"
+ timeout duration.
+</p>
+<script>
+function makeTest(...bodies) {
+ const closeScript = '<' + '/script>';
+ let src = `
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Document title</title>
+<script src="/resources/testharness.js?${Math.random()}">${closeScript}
+</head>
+
+<body>
+<div id="log"></div>`;
+ bodies.forEach((body) => {
+ src += '<script>(' + body + ')();' + closeScript;
+ });
+
+ const iframe = document.createElement('iframe');
+
+ document.body.appendChild(iframe);
+ iframe.contentDocument.write(src);
+
+ return new Promise((resolve) => {
+ window.addEventListener('message', function onMessage(e) {
+ if (e.source !== iframe.contentWindow) {
+ return;
+ }
+ if (!e.data || e.data.type !=='complete') {
+ return;
+ }
+ window.removeEventListener('message', onMessage);
+ resolve(e.data);
+ });
+
+ iframe.contentDocument.close();
+ }).then(({ tests, status }) => {
+ const summary = {
+ harness: getEnumProp(status, status.status),
+ tests: {}
+ };
+
+ tests.forEach((test) => {
+ summary.tests[test.name] = getEnumProp(test, test.status);
+ });
+
+ return summary;
+ });
+}
+
+function getEnumProp(object, value) {
+ for (let property in object) {
+ if (!/^[A-Z]+$/.test(property)) {
+ continue;
+ }
+
+ if (object[property] === value) {
+ return property;
+ }
+ }
+}
+
+(() => {
+ window.asyncTestCleanupCount1 = 0;
+ const nestedTest = makeTest(
+ () => {
+ async_test((t) => {
+ t.add_cleanup(() => window.parent.asyncTestCleanupCount1 += 1);
+ setTimeout(() => {
+ throw new Error('this error is expected');
+ });
+ }, 'test');
+ }
+ );
+ promise_test(() => {
+ return nestedTest.then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.test, 'TIMEOUT');
+ assert_equals(window.asyncTestCleanupCount1, 1);
+ });
+ }, 'uncaught exception during async_test which times out');
+})();
+
+(() => {
+ window.promiseTestCleanupCount2 = 0;
+ const nestedTest = makeTest(
+ () => {
+ promise_test((t) => {
+ t.add_cleanup(() => window.parent.promiseTestCleanupCount2 += 1);
+ setTimeout(() => {
+ throw new Error('this error is expected');
+ });
+
+ return new Promise(() => {});
+ }, 'test');
+ }
+ );
+ promise_test(() => {
+ return nestedTest.then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.test, 'TIMEOUT');
+ assert_equals(window.promiseTestCleanupCount2, 1);
+ });
+ }, 'uncaught exception during promise_test which times out');
+})();
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/exceptional-cases.html b/testing/web-platform/tests/resources/test/tests/unit/exceptional-cases.html
new file mode 100644
index 0000000000..4054d0311d
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/exceptional-cases.html
@@ -0,0 +1,392 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <script src="/resources/testharness.js"></script>
+ <title>Exceptional cases</title>
+</head>
+<body>
+<script>
+function makeTest(...bodies) {
+ const closeScript = '<' + '/script>';
+ let src = `
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Document title</title>
+<script src="/resources/testharness.js?${Math.random()}">${closeScript}
+</head>
+
+<body>
+<div id="log"></div>`;
+ bodies.forEach((body) => {
+ src += '<script>(' + body + ')();' + closeScript;
+ });
+
+ const iframe = document.createElement('iframe');
+
+ document.body.appendChild(iframe);
+ iframe.contentDocument.write(src);
+
+ return new Promise((resolve) => {
+ window.addEventListener('message', function onMessage(e) {
+ if (e.source !== iframe.contentWindow) {
+ return;
+ }
+ if (!e.data || e.data.type !=='complete') {
+ return;
+ }
+ window.removeEventListener('message', onMessage);
+ resolve(e.data);
+ });
+
+ iframe.contentDocument.close();
+ }).then(({ tests, status }) => {
+ const summary = {
+ harness: getEnumProp(status, status.status),
+ tests: {}
+ };
+
+ tests.forEach((test) => {
+ summary.tests[test.name] = getEnumProp(test, test.status);
+ });
+
+ return summary;
+ });
+}
+
+function getEnumProp(object, value) {
+ for (let property in object) {
+ if (!/^[A-Z]+$/.test(property)) {
+ continue;
+ }
+
+ if (object[property] === value) {
+ return property;
+ }
+ }
+}
+
+promise_test(() => {
+ return makeTest(
+ () => { done(); }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_array_equals(Object.keys(tests), []);
+ });
+}, 'completion signaled before testing begins');
+
+promise_test(() => {
+ return makeTest(
+ () => { assert_true(true); done(); }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_array_equals(Object.keys(tests), []);
+ });
+}, 'passing assertion before testing begins');
+
+promise_test(() => {
+ return makeTest(
+ () => { assert_false(true); }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_array_equals(Object.keys(tests), []);
+ });
+}, 'failing assertion before testing begins');
+
+promise_test(() => {
+ return makeTest(
+ () => { throw new Error('this error is expected'); }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_array_equals(Object.keys(tests), []);
+ });
+}, 'uncaught exception before testing begins');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ allow_uncaught_exception: true });
+ throw new Error('this error is expected');
+ },
+ () => {
+ test(function() {}, 'a');
+ test(function() {}, 'b');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests.a, 'PASS');
+ assert_equals(tests.b, 'PASS');
+ });
+}, 'uncaught exception with subsequent subtest');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ async_test((t) => {
+ setTimeout(() => {
+ setTimeout(() => t.done(), 0);
+ async_test((t) => { setTimeout(t.done.bind(t), 0); }, 'after');
+ throw new Error('this error is expected');
+ }, 0);
+ }, 'during');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.during, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'uncaught exception during async_test');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ promise_test(() => {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve();
+ promise_test(() => Promise.resolve(), 'after');
+ throw new Error('this error is expected');
+ }, 0);
+ });
+ }, 'during');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.during, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'uncaught exception during promise_test');
+
+promise_test(() => {
+ return makeTest(
+ () => { test(() => {}, 'before'); },
+ () => { throw new Error('this error is expected'); },
+ () => { test(() => {}, 'after'); }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'uncaught exception between tests');
+
+promise_test(() => {
+ return makeTest(
+ () => { promise_test(() => Promise.resolve(), 'before'); },
+ () => { throw new Error('this error is expected'); },
+ () => { promise_test(() => Promise.resolve(), 'after'); }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'uncaught exception between promise_tests');
+
+
+// This feature of testharness.js is only observable in browsers which
+// implement the `unhandledrejection` event.
+if ('onunhandledrejection' in window) {
+
+ promise_test(() => {
+ return makeTest(
+ () => { Promise.reject(new Error('this error is expected')); }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_array_equals(Object.keys(tests), []);
+ });
+ }, 'unhandled rejection before testing begins');
+
+ promise_test(() => {
+ return makeTest(
+ () => {
+ async_test((t) => {
+ Promise.reject(new Error('this error is expected'));
+
+ window.addEventListener('unhandledrejection', () => {
+ setTimeout(() => t.done(), 0);
+ async_test((t) => { setTimeout(t.done.bind(t), 0); }, 'after');
+ t.done();
+ });
+ }, 'during');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.during, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+ }, 'unhandled rejection during async_test');
+
+ promise_test(() => {
+ return makeTest(
+ () => {
+ promise_test(() => {
+ return new Promise((resolve) => {
+ Promise.reject(new Error('this error is expected'));
+
+ window.addEventListener('unhandledrejection', () => {
+ resolve();
+ promise_test(() => Promise.resolve(), 'after');
+ throw new Error('this error is expected');
+ }, 0);
+ });
+ }, 'during');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.during, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+ }, 'unhandled rejection during promise_test');
+
+ promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ explicit_done: true });
+ test(() => {}, 'before');
+ Promise.reject(new Error('this error is expected'));
+ window.addEventListener('unhandledrejection', () => {
+ test(() => {}, 'after');
+ done();
+ });
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_true('after' in tests);
+ });
+ }, 'unhandled rejection between tests');
+
+ promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ explicit_done: true });
+ async_test((t) => { setTimeout(t.done.bind(t), 0); }, 'before');
+ Promise.reject(new Error('this error is expected'));
+ window.addEventListener('unhandledrejection', () => {
+ async_test((t) => { setTimeout(t.done.bind(t), 0); }, 'after');
+ done();
+ });
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+ }, 'unhandled rejection between async_tests');
+
+ promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ explicit_done: true });
+ promise_test(() => Promise.resolve(), 'before');
+ Promise.reject(new Error('this error is expected'));
+ window.addEventListener('unhandledrejection', () => {
+ promise_test(() => Promise.resolve(), 'after');
+ done();
+ });
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_true('after' in tests);
+ });
+ }, 'unhandled rejection between promise_tests');
+
+ promise_test(() => {
+ return makeTest(
+ () => {
+ test((t) => {
+ t.add_cleanup(() => { throw new Error('this error is expected'); });
+ }, 'during');
+ test((t) => {}, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.during, 'PASS');
+ assert_equals(tests.after, 'NOTRUN');
+ });
+ }, 'exception in `add_cleanup` of a test');
+
+}
+
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ setup({explicit_done: true});
+ window.addEventListener('DOMContentLoaded', () => {
+ async_test((t) => {
+ t.add_cleanup(() => {
+ setTimeout(() => {
+ async_test((t) => t.done(), 'after');
+ done();
+ }, 0);
+ throw new Error('this error is expected');
+ });
+ setTimeout(t.done.bind(t), 0);
+ }, 'during');
+ });
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.during, 'PASS');
+ assert_equals(tests.after, 'NOTRUN');
+ });
+}, 'exception in `add_cleanup` of an async_test');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ promise_test((t) => {
+ t.add_cleanup(() => { throw new Error('this error is expected'); });
+ return Promise.resolve();
+ }, 'test');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.test, 'PASS');
+ });
+}, 'exception in `add_cleanup` of a promise_test');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ promise_test((t) => {
+ t.step(() => {
+ throw new Error('this error is expected');
+ });
+ }, 'test');
+ async_test((t) => t.done(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests.test, 'FAIL');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'exception in `step` of an async_test');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ promise_test((t) => {
+ t.step(() => {
+ throw new Error('this error is expected');
+ });
+
+ return new Promise(() => {});
+ }, 'test');
+
+ // This following test should be run to completion despite the fact
+ // that the promise returned by the previous test never resolves.
+ promise_test((t) => Promise.resolve(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests.test, 'FAIL');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'exception in `step` of a promise_test');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/format-value.html b/testing/web-platform/tests/resources/test/tests/unit/format-value.html
new file mode 100644
index 0000000000..13d01b81f3
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/format-value.html
@@ -0,0 +1,123 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>format_value utility function</title>
+ <meta charset="utf-8">
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+"use strict";
+
+test(function() {
+ assert_equals(format_value(null), "null");
+}, "null");
+
+test(function() {
+ assert_equals(format_value(undefined), "undefined");
+}, "undefined");
+
+test(function() {
+ assert_equals(format_value(true), "true");
+ assert_equals(format_value(false), "false");
+}, "boolean values");
+
+test(function() {
+ assert_equals(format_value(0.4), "0.4");
+ assert_equals(format_value(0), "0");
+ assert_equals(format_value(-0), "-0");
+}, "number values");
+
+test(function() {
+ assert_equals(format_value("a string"), "\"a string\"");
+ assert_equals(format_value("new\nline"), "\"new\\nline\"");
+}, "string values");
+
+test(function() {
+ var node = document.createElement("span");
+ node.setAttribute("data-foo", "bar");
+ assert_true(
+ /<span\b/i.test(format_value(node)), "element includes tag name"
+ );
+ assert_true(
+ /data-foo=["']?bar["']?/i.test(format_value(node)),
+ "element includes attributes"
+ );
+}, "node value: element node");
+
+test(function() {
+ var text = document.createTextNode("wpt");
+ assert_equals(format_value(text), "Text node \"wpt\"");
+}, "node value: text node");
+
+test(function() {
+ var node = document.createProcessingInstruction("wpt1", "wpt2");
+ assert_equals(
+ format_value(node),
+ "ProcessingInstruction node with target \"wpt1\" and data \"wpt2\""
+ );
+}, "node value: ProcessingInstruction node");
+
+test(function() {
+ var node = document.createComment("wpt");
+ assert_equals(format_value(node), "Comment node <!--wpt-->");
+}, "node value: comment node");
+
+test(function() {
+ var node = document.implementation.createDocument(
+ "application/xhtml+xml", "", null
+ );
+
+ assert_equals(format_value(node), "Document node with 0 children");
+
+ node.appendChild(document.createElement('html'));
+
+ assert_equals(format_value(node), "Document node with 1 child");
+}, "node value: document node");
+
+test(function() {
+ var node = document.implementation.createDocumentType("foo", "baz", "baz");
+
+ assert_equals(format_value(node), "DocumentType node");
+}, "node value: DocumentType node");
+
+test(function() {
+ var node = document.createDocumentFragment();
+
+ assert_equals(format_value(node), "DocumentFragment node with 0 children");
+
+ node.appendChild(document.createElement("span"));
+
+ assert_equals(format_value(node), "DocumentFragment node with 1 child");
+
+ node.appendChild(document.createElement("span"));
+
+ assert_equals(format_value(node), "DocumentFragment node with 2 children");
+}, "node value: DocumentFragment node");
+
+test(function() {
+ assert_equals(format_value(Symbol("wpt")), "symbol \"Symbol(wpt)\"");
+}, "symbol value");
+
+test(function() {
+ assert_equals(format_value([]), "[]");
+ assert_equals(format_value(["one"]), "[\"one\"]");
+ assert_equals(format_value(["one", "two"]), "[\"one\", \"two\"]");
+}, "array values");
+
+test(function() {
+ var obj = {
+ toString: function() {
+ throw "wpt";
+ }
+ };
+
+ assert_equals(
+ format_value(obj), "[stringifying object threw wpt with type string]"
+ );
+}, "object value with faulty `toString`");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/helpers.js b/testing/web-platform/tests/resources/test/tests/unit/helpers.js
new file mode 100644
index 0000000000..ca378a27c9
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/helpers.js
@@ -0,0 +1,21 @@
+// Helper for testing assertion failure cases for a testharness.js API
+//
+// The `assert_throws_*` functions cannot be used for this purpose because they
+// always fail in response to AssertionError exceptions, even when this is
+// expressed as the expected error.
+function test_failure(fn, name) {
+ test(function() {
+ try {
+ fn();
+ } catch (err) {
+ if (err instanceof AssertionError) {
+ return;
+ }
+ throw new AssertionError('Expected an AssertionError, but' + err);
+ }
+ throw new AssertionError(
+ 'Expected an AssertionError, but no error was thrown'
+ );
+ }, name);
+}
+
diff --git a/testing/web-platform/tests/resources/test/tests/unit/late-test.html b/testing/web-platform/tests/resources/test/tests/unit/late-test.html
new file mode 100644
index 0000000000..693d7e3c81
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/late-test.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test declared after harness completion</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<p>This test simulates an automated test running scenario, where the test
+results emitted by testharness.js may be interpreted after some delay. It is
+intended to demonstrate that in such cases, any additional tests which are
+executed during that delay are <em>not</em> included in the dataset.</p>
+
+<p>Although these "late" tests are likely an indication of a mistake in test
+design, they cannot be detected deterministically, so in the interest of
+stability, they should be silently tolerated.</p>
+<script>
+async_test(function(t) {
+ var source = [
+ "<div id='log'></div>",
+ "<script src='/resources/testharness.js'></" + "script>",
+ "<script src='/resources/testharnessreport.js'></" + "script>",
+ "<script>",
+ "parent.childReady(window);",
+ "setup({ explicit_done: true });",
+ "test(function() {}, 'acceptable test');",
+ "onload = function() {",
+ " done();",
+ " test(function() {}, 'this test is late and should be ignored');",
+ "};",
+ "</" + "script>"
+ ].join("\n");
+ var iframe = document.createElement("iframe");
+
+ document.body.appendChild(iframe);
+ window.childReady = t.step_func(function(childWindow) {
+ childWindow.add_completion_callback(t.step_func(function(tests, status) {
+ t.step_timeout(t.step_func(function() {
+ assert_equals(tests.length, 1);
+ assert_equals(tests[0].name, "acceptable test");
+ assert_equals(status.status, status.OK);
+ t.done();
+ }), 0);
+ }));
+ });
+
+ iframe.contentDocument.open();
+ iframe.contentDocument.write(source);
+ iframe.contentDocument.close();
+});
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/promise_setup-timeout.html b/testing/web-platform/tests/resources/test/tests/unit/promise_setup-timeout.html
new file mode 100644
index 0000000000..c4947feef4
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/promise_setup-timeout.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <script src="/resources/testharness.js"></script>
+ <script src="../../nested-testharness.js"></script>
+ <title>promise_setup - timeout</title>
+</head>
+<body>
+<script>
+'use strict';
+promise_test(() => {
+ return makeTest(
+ () => {
+ test(() => {}, 'before');
+ promise_setup(() => new Promise(() => {}));
+ promise_test(() => Promise.resolve(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'TIMEOUT');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'NOTRUN');
+ });
+}, 'timeout when returned promise does not settle');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/promise_setup.html b/testing/web-platform/tests/resources/test/tests/unit/promise_setup.html
new file mode 100644
index 0000000000..2abb10a476
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/promise_setup.html
@@ -0,0 +1,333 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="/resources/testharness.js"></script>
+ <script src="../../nested-testharness.js"></script>
+ <title>promise_setup</title>
+</head>
+<body>
+<script>
+'use strict';
+promise_test(() => {
+ return makeTest(
+ () => {
+ // Ensure that the harness error is the result of explicit error
+ // handling
+ setup({ allow_uncaught_exception: true });
+
+ test(() => {}, 'before');
+ promise_setup({});
+ promise_test(() => Promise.resolve(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, undefined);
+ });
+}, 'Error when no function provided');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ test(() => {}, 'before');
+ promise_setup(() => Promise.resolve(), {});
+ promise_test(() => Promise.resolve(), 'after');
+ throw new Error('this error is expected');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'Does not apply unspecified configuration properties');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ var properties = {
+ allow_uncaught_exception: true
+ };
+ test(() => {}, 'before');
+ promise_setup(() => Promise.resolve(), properties);
+ promise_test(() => Promise.resolve(), 'after');
+ throw new Error('this error is expected');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'Ignores configuration properties when some tests have already run');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ var properties = {
+ allow_uncaught_exception: true
+ };
+ promise_setup(() => Promise.resolve(), properties);
+ promise_test(() => {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve();
+ throw new Error('this error is expected');
+ });
+ });
+ }, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'Honors configuration properties');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ // Ensure that the harness error is the result of explicit error
+ // handling
+ setup({ allow_uncaught_exception: true });
+
+ test(() => {}, 'before');
+ promise_setup(() => { throw new Error('this error is expected'); });
+ promise_test(() => Promise.resolve(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'NOTRUN');
+ });
+}, 'Error for synchronous exceptions');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ // Ensure that the harness error is the result of explicit error
+ // handling
+ setup({ allow_uncaught_exception: true });
+
+ test(() => {}, 'before');
+ promise_setup(() => undefined);
+ promise_test(() => Promise.resolve(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'NOTRUN');
+ });
+}, 'Error for missing return value');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ // Ensure that the harness error is the result of explicit error
+ // handling
+ setup({ allow_uncaught_exception: true });
+
+ test(() => {}, 'before');
+ var noThen = Promise.resolve();
+ noThen.then = undefined;
+ promise_setup(() => noThen);
+ promise_test(() => Promise.resolve(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'NOTRUN');
+ });
+}, 'Error for non-thenable return value');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ // Ensure that the harness error is the result of explicit error
+ // handling
+ setup({ allow_uncaught_exception: true });
+
+ test(() => {}, 'before');
+ var poisonedThen = {
+ get then() {
+ throw new Error('this error is expected');
+ }
+ };
+ promise_setup(() => poisonedThen);
+ promise_test(() => Promise.resolve(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'NOTRUN');
+ });
+}, 'Error for "poisoned" `then` property');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ // Ensure that the harness error is the result of explicit error
+ // handling
+ setup({ allow_uncaught_exception: true });
+
+ test(() => {}, 'before');
+ var badThen = {
+ then() {
+ throw new Error('this error is expected');
+ }
+ };
+ promise_setup(() => badThen);
+ promise_test(() => Promise.resolve(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'NOTRUN');
+ });
+}, 'Error for synchronous error from `then` method');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ // Ensure that the harness error is the result of explicit error
+ // handling
+ setup({ allow_uncaught_exception: true });
+
+ test(() => {}, 'before');
+ promise_setup(() => Promise.resolve());
+ test(() => {}, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, undefined);
+ });
+}, 'Error for subsequent invocation of `test`');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ // Ensure that the harness error is the result of explicit error
+ // handling
+ setup({ allow_uncaught_exception: true });
+
+ test(() => {}, 'before');
+ promise_setup(() => Promise.resolve());
+ async_test((t) => t.done(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, undefined);
+ });
+}, 'Error for subsequent invocation of `async_test`');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ // Ensure that the harness error is the result of explicit error
+ // handling
+ setup({ allow_uncaught_exception: true });
+
+ test(() => {}, 'before');
+ promise_setup(() => Promise.reject());
+ promise_test(() => Promise.resolve(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'NOTRUN');
+ });
+}, 'Error for rejected promise');
+
+promise_test(() => {
+ var expected_sequence = [
+ 'test body',
+ 'promise_setup begin',
+ 'promise_setup end',
+ 'promise_test body'
+ ];
+ var actual_sequence = window.actual_sequence = [];
+
+ return makeTest(
+ () => {
+ test(() => { parent.actual_sequence.push('test body'); }, 'before');
+ promise_setup(() => {
+ parent.actual_sequence.push('promise_setup begin');
+
+ return Promise.resolve()
+ .then(() => new Promise((resolve) => setTimeout(resolve, 300)))
+ .then(() => parent.actual_sequence.push('promise_setup end'));
+ });
+ promise_test(() => {
+ parent.actual_sequence.push('promise_test body');
+ return Promise.resolve();
+ }, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ assert_array_equals(actual_sequence, expected_sequence);
+ });
+}, 'Waits for promise to settle');
+
+promise_test(() => {
+ var expected_sequence = [
+ 'promise_test 1 begin',
+ 'promise_test 1 end',
+ 'promise_setup begin',
+ 'promise_setup end',
+ 'promise_test 2 body'
+ ];
+ var actual_sequence = window.actual_sequence = [];
+
+ return makeTest(
+ () => {
+ promise_test((t) => {
+ parent.actual_sequence.push('promise_test 1 begin');
+
+ return Promise.resolve()
+ .then(() => new Promise((resolve) => t.step_timeout(resolve, 300)))
+ .then(() => parent.actual_sequence.push('promise_test 1 end'));
+ }, 'before');
+ promise_setup(() => {
+ parent.actual_sequence.push('promise_setup begin');
+
+ return Promise.resolve()
+ .then(() => new Promise((resolve) => setTimeout(resolve, 300)))
+ .then(() => parent.actual_sequence.push('promise_setup end'));
+ });
+ promise_test(() => {
+ parent.actual_sequence.push('promise_test 2 body');
+ return Promise.resolve();
+ }, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ assert_array_equals(actual_sequence, expected_sequence);
+ });
+}, 'Waits for existing promise_test to complete');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ var properties = { allow_uncaught_exception: true };
+ promise_test(() => {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve();
+ throw new Error('this error is expected');
+ });
+ });
+ }, 'before');
+ promise_setup(() => Promise.resolve(), properties);
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ });
+}, 'Defers application of setup properties');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/single_test.html b/testing/web-platform/tests/resources/test/tests/unit/single_test.html
new file mode 100644
index 0000000000..ff766e66ce
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/single_test.html
@@ -0,0 +1,94 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <script src="/resources/testharness.js"></script>
+ <script src="../../nested-testharness.js"></script>
+ <title>single_test</title>
+</head>
+<body>
+<script>
+promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ single_test: true });
+ done();
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'Expected usage');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ single_test: true });
+ throw new Error('this error is expected');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'Uncaught exception');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ single_test: true });
+ Promise.reject(new Error('this error is expected'));
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'Unhandled rejection');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ single_test: true });
+ test(function() {}, 'sync test');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ assert_equals(
+ Object.keys(tests).length, 1, 'no additional subtests created'
+ );
+ });
+}, 'Erroneous usage: subtest declaration (synchronous test)');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ single_test: true });
+ async_test(function(t) { t.done(); }, 'async test');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ assert_equals(
+ Object.keys(tests).length, 1, 'no additional subtests created'
+ );
+ });
+}, 'Erroneous usage: subtest declaration (asynchronous test)');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ single_test: true });
+ promise_test(function() { return Promise.resolve(); }, 'promise test');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ assert_equals(
+ Object.keys(tests).length, 1, 'no additional subtests created'
+ );
+ });
+}, 'Erroneous usage: subtest declaration (promise test)');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/test-return-restrictions.html b/testing/web-platform/tests/resources/test/tests/unit/test-return-restrictions.html
new file mode 100644
index 0000000000..0295c5214d
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/test-return-restrictions.html
@@ -0,0 +1,156 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="/resources/testharness.js"></script>
+ <title>Restrictions on return value from `test`</title>
+</head>
+<body>
+<script>
+function makeTest(...bodies) {
+ const closeScript = '<' + '/script>';
+ let src = `
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Document title</title>
+<script src="/resources/testharness.js?${Math.random()}">${closeScript}
+</head>
+
+<body>
+<div id="log"></div>`;
+ bodies.forEach((body) => {
+ src += '<script>(' + body + ')();' + closeScript;
+ });
+
+ const iframe = document.createElement('iframe');
+
+ document.body.appendChild(iframe);
+ iframe.contentDocument.write(src);
+
+ return new Promise((resolve) => {
+ window.addEventListener('message', function onMessage(e) {
+ if (e.source !== iframe.contentWindow) {
+ return;
+ }
+ if (!e.data || e.data.type !=='complete') {
+ return;
+ }
+ window.removeEventListener('message', onMessage);
+ resolve(e.data);
+ });
+
+ iframe.contentDocument.close();
+ }).then(({ tests, status }) => {
+ const summary = {
+ harness: {
+ status: getEnumProp(status, status.status),
+ message: status.message
+ },
+ tests: {}
+ };
+
+ tests.forEach((test) => {
+ summary.tests[test.name] = getEnumProp(test, test.status);
+ });
+
+ return summary;
+ });
+}
+
+function getEnumProp(object, value) {
+ for (let property in object) {
+ if (!/^[A-Z]+$/.test(property)) {
+ continue;
+ }
+
+ if (object[property] === value) {
+ return property;
+ }
+ }
+}
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ test(() => undefined, 'before');
+ test(() => null, 'null');
+ test(() => undefined, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'ERROR');
+ assert_equals(
+ harness.message,
+ 'Test named "null" passed a function to `test` that returned a value.'
+ );
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.null, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'test returning `null`');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ test(() => undefined, 'before');
+ test(() => ({}), 'object');
+ test(() => undefined, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'ERROR');
+ assert_equals(
+ harness.message,
+ 'Test named "object" passed a function to `test` that returned a value.'
+ );
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.object, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'test returning an ordinary object');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ test(() => undefined, 'before');
+ test(() => Promise.resolve(5), 'thenable');
+ test(() => undefined, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'ERROR');
+ assert_equals(
+ harness.message,
+ 'Test named "thenable" passed a function to `test` that returned a value. ' +
+ 'Consider using `promise_test` instead when using Promises or async/await.'
+ );
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.thenable, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'test returning a thenable object');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ test(() => undefined, 'before');
+ test(() => {
+ const iframe = document.createElement('iframe');
+ iframe.setAttribute('sandbox', '');
+ document.body.appendChild(iframe);
+ return iframe.contentWindow;
+ }, 'restricted');
+ test(() => undefined, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'ERROR');
+ assert_equals(
+ harness.message,
+ 'Test named "restricted" passed a function to `test` that returned a value.'
+ );
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.restricted, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'test returning a restricted object');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/throwing-assertions.html b/testing/web-platform/tests/resources/test/tests/unit/throwing-assertions.html
new file mode 100644
index 0000000000..a36a56043c
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/throwing-assertions.html
@@ -0,0 +1,268 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <script src="/resources/testharness.js"></script>
+ <title>Test the methods that make assertions about exceptions</title>
+</head>
+<body>
+<script>
+function makeTest(...bodies) {
+ const closeScript = '<' + '/script>';
+ let src = `
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Document title</title>
+<script src="/resources/testharness.js?${Math.random()}">${closeScript}
+</head>
+
+<body>
+<div id="log"></div>`;
+ bodies.forEach((body) => {
+ src += '<script>(' + body + ')();' + closeScript;
+ });
+
+ const iframe = document.createElement('iframe');
+
+ document.body.appendChild(iframe);
+ iframe.contentDocument.write(src);
+
+ return new Promise((resolve) => {
+ window.addEventListener('message', function onMessage(e) {
+ if (e.source !== iframe.contentWindow) {
+ return;
+ }
+ if (!e.data || e.data.type !=='complete') {
+ return;
+ }
+ window.removeEventListener('message', onMessage);
+ resolve(e.data);
+ });
+
+ iframe.contentDocument.close();
+ }).then(({ tests, status }) => {
+ const summary = {
+ harness: getEnumProp(status, status.status),
+ tests: {}
+ };
+
+ tests.forEach((test) => {
+ summary.tests[test.name] = getEnumProp(test, test.status);
+ });
+
+ return summary;
+ });
+}
+
+function getEnumProp(object, value) {
+ for (let property in object) {
+ if (!/^[A-Z]+$/.test(property)) {
+ continue;
+ }
+
+ if (object[property] === value) {
+ return property;
+ }
+ }
+}
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_js(TypeError, () => { throw new TypeError(); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'assert_throws_js on a TypeError');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_js(RangeError, () => { throw new RangeError(); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'assert_throws_js on a RangeError');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_js(TypeError, () => { throw new RangeError(); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'assert_throws_js on a TypeError when RangeError is thrown');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_js(Error, () => { throw new TypeError(); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'assert_throws_js on an Error when TypeError is thrown');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_js(Error,
+ () => { throw new DOMException("hello", "Error"); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'assert_throws_js on an Error when a DOMException is thrown');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_js(SyntaxError,
+ () => { throw new DOMException("hey", "SyntaxError"); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'assert_throws_js on a SyntaxError when a DOMException is thrown');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_dom("SyntaxError",
+ () => { throw new DOMException("x", "SyntaxError"); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'assert_throws_dom basic sanity');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_dom(12,
+ () => { throw new DOMException("x", "SyntaxError"); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'assert_throws_dom with numeric code');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_dom("SYNTAX_ERR",
+ () => { throw new DOMException("x", "SyntaxError"); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'assert_throws_dom with string name for code');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_dom("DataError",
+ () => { throw new DOMException("x", "DataError"); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'assert_throws_dom for a code-less DOMException type');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_dom("NoSuchError",
+ () => { throw new DOMException("x", "NoSuchError"); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'assert_throws_dom for a nonexistent DOMException type');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_dom("SyntaxError", () => { throw new SyntaxError(); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'assert_throws_dom when a non-DOM exception is thrown');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_exactly(5, () => { throw 5; });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'assert_throws_exactly with number');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_exactly("foo", () => { throw "foo"; });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'assert_throws_exactly with string');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_exactly({}, () => { throw {}; });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'assert_throws_exactly with different objects');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ var obj = {};
+ assert_throws_exactly(obj, () => { throw obj; });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'assert_throws_exactly with same object');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_exactly(TypeError, () => { throw new TypeError; });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'assert_throws_exactly with bogus TypeError bits ');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/unpaired-surrogates.html b/testing/web-platform/tests/resources/test/tests/unit/unpaired-surrogates.html
new file mode 100644
index 0000000000..b232111326
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/unpaired-surrogates.html
@@ -0,0 +1,143 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="/resources/testharness.js"></script>
+ <title>Restrictions on return value from `test`</title>
+</head>
+<body>
+<script>
+function makeTest(...bodies) {
+ const closeScript = '<' + '/script>';
+ let src = `
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Document title</title>
+<script src="/resources/testharness.js?${Math.random()}">${closeScript}
+</head>
+
+<body>
+<div id="log"></div>`;
+ bodies.forEach((body) => {
+ src += '<script>(' + body + ')();' + closeScript;
+ });
+
+ const iframe = document.createElement('iframe');
+
+ document.body.appendChild(iframe);
+ iframe.contentDocument.write(src);
+
+ return new Promise((resolve) => {
+ window.addEventListener('message', function onMessage(e) {
+ if (e.source !== iframe.contentWindow) {
+ return;
+ }
+ if (!e.data || e.data.type !=='complete') {
+ return;
+ }
+ window.removeEventListener('message', onMessage);
+ resolve(e.data);
+ });
+
+ iframe.contentDocument.close();
+ }).then(({ tests, status }) => {
+ const summary = {
+ harness: {
+ status: getEnumProp(status, status.status),
+ message: status.message
+ },
+ tests: {}
+ };
+
+ tests.forEach((test) => {
+ summary.tests[test.name] = getEnumProp(test, test.status);
+ });
+
+ return summary;
+ });
+}
+
+function getEnumProp(object, value) {
+ for (let property in object) {
+ if (!/^[A-Z]+$/.test(property)) {
+ continue;
+ }
+
+ if (object[property] === value) {
+ return property;
+ }
+ }
+}
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ test(() => {}, 'before');
+ test(() => {}, 'U+d7ff is not modified: \ud7ff');
+ test(() => {}, 'U+e000 is not modified: \ue000');
+ test(() => {}, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'OK');
+ assert_equals(harness.message, null);
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests['U+d7ff is not modified: \ud7ff'], 'PASS');
+ assert_equals(tests['U+e000 is not modified: \ue000'], 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'sub-test names which include valid code units');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ test(() => {}, 'before');
+ test(() => {}, 'U+d800U+dfff is not modified: \ud800\udfff');
+ test(() => {}, 'U+dbffU+dc00 is not modified: \udbff\udc00');
+ test(() => {}, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'OK');
+ assert_equals(harness.message, null);
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests['U+d800U+dfff is not modified: \ud800\udfff'], 'PASS');
+ assert_equals(tests['U+dbffU+dc00 is not modified: \udbff\udc00'], 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'sub-test names which include paired surrogates');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ test(() => {}, 'before');
+ test(() => {}, 'U+d800 must be sanitized: \ud800');
+ test(() => {}, 'U+d800U+d801 must be sanitized: \ud800\ud801');
+ test(() => {}, 'U+dfff must be sanitized: \udfff');
+ test(() => {}, 'U+dc00U+d800U+dc00U+d800 must be sanitized: \udc00\ud800\udc00\ud800');
+ test(() => {}, 'U+dbffU+dfffU+dfff must be sanitized: \udbff\udfff\udfff');
+ test(() => {}, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'OK');
+ assert_equals(harness.message, null);
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests['U+d800 must be sanitized: U+d800'], 'PASS');
+ assert_equals(tests['U+dfff must be sanitized: U+dfff'], 'PASS');
+ assert_equals(
+ tests['U+d800U+d801 must be sanitized: U+d800U+d801'],
+ 'PASS'
+ );
+ assert_equals(
+ tests['U+dc00U+d800U+dc00U+d800 must be sanitized: U+dc00\ud800\udc00U+d800'],
+ 'PASS'
+ );
+ assert_equals(
+ tests['U+dbffU+dfffU+dfff must be sanitized: \udbff\udfffU+dfff'],
+ 'PASS'
+ );
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'sub-test names which include unpaired surrogates');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tox.ini b/testing/web-platform/tests/resources/test/tox.ini
new file mode 100644
index 0000000000..4fbeb67fb5
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tox.ini
@@ -0,0 +1,13 @@
+[tox]
+envlist = py37,py38,py39,py310
+skipsdist=True
+
+[testenv]
+passenv=DISPLAY # Necessary for the spawned GeckoDriver process to connect to
+ # the appropriate display.
+
+deps =
+ -r{toxinidir}/../../tools/requirements_pytest.txt
+ -r{toxinidir}/requirements.txt
+
+commands = pytest -vv {posargs}
diff --git a/testing/web-platform/tests/resources/test/wptserver.py b/testing/web-platform/tests/resources/test/wptserver.py
new file mode 100644
index 0000000000..1f913dd96d
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/wptserver.py
@@ -0,0 +1,58 @@
+import logging
+import os
+import subprocess
+import time
+import sys
+import urllib
+
+
+class WPTServer(object):
+ def __init__(self, wpt_root):
+ self.logger = logging.getLogger()
+ self.wpt_root = wpt_root
+
+ # This is a terrible hack to get the default config of wptserve.
+ sys.path.insert(0, os.path.join(wpt_root, "tools"))
+ from serve.serve import build_config
+ with build_config(self.logger) as config:
+ self.host = config["browser_host"]
+ self.http_port = config["ports"]["http"][0]
+ self.https_port = config["ports"]["https"][0]
+
+ self.base_url = 'http://%s:%s' % (self.host, self.http_port)
+ self.https_base_url = 'https://%s:%s' % (self.host, self.https_port)
+
+ def start(self, ssl_context):
+ self.devnull = open(os.devnull, 'w')
+ wptserve_cmd = [os.path.join(self.wpt_root, 'wpt'), 'serve']
+ if sys.executable:
+ wptserve_cmd[0:0] = [sys.executable]
+ self.logger.info('Executing %s' % ' '.join(wptserve_cmd))
+ self.proc = subprocess.Popen(
+ wptserve_cmd,
+ stderr=self.devnull,
+ cwd=self.wpt_root)
+
+ for retry in range(5):
+ # Exponential backoff.
+ time.sleep(2 ** retry)
+ exit_code = self.proc.poll()
+ if exit_code != None:
+ logging.warning('Command "%s" exited with %s', ' '.join(wptserve_cmd), exit_code)
+ break
+ try:
+ urllib.request.urlopen(self.base_url, timeout=1)
+ urllib.request.urlopen(self.https_base_url, timeout=1, context=ssl_context)
+ return
+ except urllib.error.URLError:
+ pass
+
+ raise Exception('Could not start wptserve on %s' % self.base_url)
+
+ def stop(self):
+ self.proc.terminate()
+ self.proc.wait()
+ self.devnull.close()
+
+ def url(self, abs_path):
+ return self.https_base_url + '/' + os.path.relpath(abs_path, self.wpt_root)
diff --git a/testing/web-platform/tests/resources/testdriver-actions.js b/testing/web-platform/tests/resources/testdriver-actions.js
new file mode 100644
index 0000000000..3e5ba74b4c
--- /dev/null
+++ b/testing/web-platform/tests/resources/testdriver-actions.js
@@ -0,0 +1,599 @@
+(function() {
+ let sourceNameIdx = 0;
+
+ /**
+ * @class
+ * Builder for creating a sequence of actions
+ *
+ *
+ * The actions are dispatched once
+ * :js:func:`test_driver.Actions.send` is called. This returns a
+ * promise which resolves once the actions are complete.
+ *
+ * The other methods on :js:class:`test_driver.Actions` object are
+ * used to build the sequence of actions that will be sent. These
+ * return the `Actions` object itself, so the actions sequence can
+ * be constructed by chaining method calls.
+ *
+ * Internally :js:func:`test_driver.Actions.send` invokes
+ * :js:func:`test_driver.action_sequence`.
+ *
+ * @example
+ * let text_box = document.getElementById("text");
+ *
+ * let actions = new test_driver.Actions()
+ * .pointerMove(0, 0, {origin: text_box})
+ * .pointerDown()
+ * .pointerUp()
+ * .addTick()
+ * .keyDown("p")
+ * .keyUp("p");
+ *
+ * await actions.send();
+ *
+ * @param {number} [defaultTickDuration] - The default duration of a
+ * tick. Be default this is set ot 16ms, which is one frame time
+ * based on 60Hz display.
+ */
+ function Actions(defaultTickDuration=16) {
+ this.sourceTypes = new Map([["key", KeySource],
+ ["pointer", PointerSource],
+ ["wheel", WheelSource],
+ ["none", GeneralSource]]);
+ this.sources = new Map();
+ this.sourceOrder = [];
+ for (let sourceType of this.sourceTypes.keys()) {
+ this.sources.set(sourceType, new Map());
+ }
+ this.currentSources = new Map();
+ for (let sourceType of this.sourceTypes.keys()) {
+ this.currentSources.set(sourceType, null);
+ }
+ this.createSource("none");
+ this.tickIdx = 0;
+ this.defaultTickDuration = defaultTickDuration;
+ this.context = null;
+ }
+
+ Actions.prototype = {
+ ButtonType: {
+ LEFT: 0,
+ MIDDLE: 1,
+ RIGHT: 2,
+ BACK: 3,
+ FORWARD: 4,
+ },
+
+ /**
+ * Generate the action sequence suitable for passing to
+ * test_driver.action_sequence
+ *
+ * @returns {Array} Array of WebDriver-compatible actions sequences
+ */
+ serialize: function() {
+ let actions = [];
+ for (let [sourceType, sourceName] of this.sourceOrder) {
+ let source = this.sources.get(sourceType).get(sourceName);
+ let serialized = source.serialize(this.tickIdx + 1, this.defaultTickDuration);
+ if (serialized) {
+ serialized.id = sourceName;
+ actions.push(serialized);
+ }
+ }
+ return actions;
+ },
+
+ /**
+ * Generate and send the action sequence
+ *
+ * @returns {Promise} fulfilled after the sequence is executed,
+ * rejected if any actions fail.
+ */
+ send: function() {
+ let actions;
+ try {
+ actions = this.serialize();
+ } catch(e) {
+ return Promise.reject(e);
+ }
+ return test_driver.action_sequence(actions, this.context);
+ },
+
+ /**
+ * Set the context for the actions
+ *
+ * @param {WindowProxy} context - Context in which to run the action sequence
+ */
+ setContext: function(context) {
+ this.context = context;
+ return this;
+ },
+
+ /**
+ * Get the action source with a particular source type and name.
+ * If no name is passed, a new source with the given type is
+ * created.
+ *
+ * @param {String} type - Source type ('none', 'key', 'pointer', or 'wheel')
+ * @param {String?} name - Name of the source
+ * @returns {Source} Source object for that source.
+ */
+ getSource: function(type, name) {
+ if (!this.sources.has(type)) {
+ throw new Error(`${type} is not a valid action type`);
+ }
+ if (name === null || name === undefined) {
+ name = this.currentSources.get(type);
+ }
+ if (name === null || name === undefined) {
+ return this.createSource(type, null);
+ }
+ return this.sources.get(type).get(name);
+ },
+
+ setSource: function(type, name) {
+ if (!this.sources.has(type)) {
+ throw new Error(`${type} is not a valid action type`);
+ }
+ if (!this.sources.get(type).has(name)) {
+ throw new Error(`${name} is not a valid source for ${type}`);
+ }
+ this.currentSources.set(type, name);
+ return this;
+ },
+
+ /**
+ * Add a new key input source with the given name
+ *
+ * @param {String} name - Name of the key source
+ * @param {Bool} set - Set source as the default key source
+ * @returns {Actions}
+ */
+ addKeyboard: function(name, set=true) {
+ this.createSource("key", name);
+ if (set) {
+ this.setKeyboard(name);
+ }
+ return this;
+ },
+
+ /**
+ * Set the current default key source
+ *
+ * @param {String} name - Name of the key source
+ * @returns {Actions}
+ */
+ setKeyboard: function(name) {
+ this.setSource("key", name);
+ return this;
+ },
+
+ /**
+ * Add a new pointer input source with the given name
+ *
+ * @param {String} type - Name of the pointer source
+ * @param {String} pointerType - Type of pointing device
+ * @param {Bool} set - Set source as the default pointer source
+ * @returns {Actions}
+ */
+ addPointer: function(name, pointerType="mouse", set=true) {
+ this.createSource("pointer", name, {pointerType: pointerType});
+ if (set) {
+ this.setPointer(name);
+ }
+ return this;
+ },
+
+ /**
+ * Set the current default pointer source
+ *
+ * @param {String} name - Name of the pointer source
+ * @returns {Actions}
+ */
+ setPointer: function(name) {
+ this.setSource("pointer", name);
+ return this;
+ },
+
+ /**
+ * Add a new wheel input source with the given name
+ *
+ * @param {String} type - Name of the wheel source
+ * @param {Bool} set - Set source as the default wheel source
+ * @returns {Actions}
+ */
+ addWheel: function(name, set=true) {
+ this.createSource("wheel", name);
+ if (set) {
+ this.setWheel(name);
+ }
+ return this;
+ },
+
+ /**
+ * Set the current default wheel source
+ *
+ * @param {String} name - Name of the wheel source
+ * @returns {Actions}
+ */
+ setWheel: function(name) {
+ this.setSource("wheel", name);
+ return this;
+ },
+
+ createSource: function(type, name, parameters={}) {
+ if (!this.sources.has(type)) {
+ throw new Error(`${type} is not a valid action type`);
+ }
+ let sourceNames = new Set();
+ for (let [_, name] of this.sourceOrder) {
+ sourceNames.add(name);
+ }
+ if (!name) {
+ do {
+ name = "" + sourceNameIdx++;
+ } while (sourceNames.has(name))
+ } else {
+ if (sourceNames.has(name)) {
+ throw new Error(`Alreay have a source of type ${type} named ${name}.`);
+ }
+ }
+ this.sources.get(type).set(name, new (this.sourceTypes.get(type))(parameters));
+ this.currentSources.set(type, name);
+ this.sourceOrder.push([type, name]);
+ return this.sources.get(type).get(name);
+ },
+
+ /**
+ * Insert a new actions tick
+ *
+ * @param {Number?} duration - Minimum length of the tick in ms.
+ * @returns {Actions}
+ */
+ addTick: function(duration) {
+ this.tickIdx += 1;
+ if (duration) {
+ this.pause(duration);
+ }
+ return this;
+ },
+
+ /**
+ * Add a pause to the current tick
+ *
+ * @param {Number?} duration - Minimum length of the tick in ms.
+ * @param {String} sourceType - source type
+ * @param {String?} sourceName - Named key, pointer or wheel source to use
+ * or null for the default key, pointer or
+ * wheel source
+ * @returns {Actions}
+ */
+ pause: function(duration=0, sourceType="none", {sourceName=null}={}) {
+ if (sourceType=="none")
+ this.getSource("none").addPause(this, duration);
+ else
+ this.getSource(sourceType, sourceName).addPause(this, duration);
+ return this;
+ },
+
+ /**
+ * Create a keyDown event for the current default key source
+ *
+ * @param {String} key - Key to press
+ * @param {String?} sourceName - Named key source to use or null for the default key source
+ * @returns {Actions}
+ */
+ keyDown: function(key, {sourceName=null}={}) {
+ let source = this.getSource("key", sourceName);
+ source.keyDown(this, key);
+ return this;
+ },
+
+ /**
+ * Create a keyDown event for the current default key source
+ *
+ * @param {String} key - Key to release
+ * @param {String?} sourceName - Named key source to use or null for the default key source
+ * @returns {Actions}
+ */
+ keyUp: function(key, {sourceName=null}={}) {
+ let source = this.getSource("key", sourceName);
+ source.keyUp(this, key);
+ return this;
+ },
+
+ /**
+ * Create a pointerDown event for the current default pointer source
+ *
+ * @param {String} button - Button to press
+ * @param {String?} sourceName - Named pointer source to use or null for the default
+ * pointer source
+ * @returns {Actions}
+ */
+ pointerDown: function({button=this.ButtonType.LEFT, sourceName=null,
+ width, height, pressure, tangentialPressure,
+ tiltX, tiltY, twist, altitudeAngle, azimuthAngle}={}) {
+ let source = this.getSource("pointer", sourceName);
+ source.pointerDown(this, button, width, height, pressure, tangentialPressure,
+ tiltX, tiltY, twist, altitudeAngle, azimuthAngle);
+ return this;
+ },
+
+ /**
+ * Create a pointerUp event for the current default pointer source
+ *
+ * @param {String} button - Button to release
+ * @param {String?} sourceName - Named pointer source to use or null for the default pointer
+ * source
+ * @returns {Actions}
+ */
+ pointerUp: function({button=this.ButtonType.LEFT, sourceName=null}={}) {
+ let source = this.getSource("pointer", sourceName);
+ source.pointerUp(this, button);
+ return this;
+ },
+
+ /**
+ * Create a move event for the current default pointer source
+ *
+ * @param {Number} x - Destination x coordinate
+ * @param {Number} y - Destination y coordinate
+ * @param {String|Element} origin - Origin of the coordinate system.
+ * Either "pointer", "viewport" or an Element
+ * @param {Number?} duration - Time in ms for the move
+ * @param {String?} sourceName - Named pointer source to use or null for the default pointer
+ * source
+ * @returns {Actions}
+ */
+ pointerMove: function(x, y,
+ {origin="viewport", duration, sourceName=null,
+ width, height, pressure, tangentialPressure,
+ tiltX, tiltY, twist, altitudeAngle, azimuthAngle}={}) {
+ let source = this.getSource("pointer", sourceName);
+ source.pointerMove(this, x, y, duration, origin, width, height, pressure,
+ tangentialPressure, tiltX, tiltY, twist, altitudeAngle,
+ azimuthAngle);
+ return this;
+ },
+
+ /**
+ * Create a scroll event for the current default wheel source
+ *
+ * @param {Number} x - mouse cursor x coordinate
+ * @param {Number} y - mouse cursor y coordinate
+ * @param {Number} deltaX - scroll delta value along the x-axis in pixels
+ * @param {Number} deltaY - scroll delta value along the y-axis in pixels
+ * @param {String|Element} origin - Origin of the coordinate system.
+ * Either "viewport" or an Element
+ * @param {Number?} duration - Time in ms for the scroll
+ * @param {String?} sourceName - Named wheel source to use or null for the
+ * default wheel source
+ * @returns {Actions}
+ */
+ scroll: function(x, y, deltaX, deltaY,
+ {origin="viewport", duration, sourceName=null}={}) {
+ let source = this.getSource("wheel", sourceName);
+ source.scroll(this, x, y, deltaX, deltaY, duration, origin);
+ return this;
+ },
+ };
+
+ function GeneralSource() {
+ this.actions = new Map();
+ }
+
+ GeneralSource.prototype = {
+ serialize: function(tickCount, defaultTickDuration) {
+ let actions = [];
+ let data = {"type": "none", "actions": actions};
+ for (let i=0; i<tickCount; i++) {
+ if (this.actions.has(i)) {
+ actions.push(this.actions.get(i));
+ } else {
+ actions.push({"type": "pause", duration: defaultTickDuration});
+ }
+ }
+ return data;
+ },
+
+ addPause: function(actions, duration) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ throw new Error(`Already have a pause action for the current tick`);
+ }
+ this.actions.set(tick, {type: "pause", duration: duration});
+ },
+ };
+
+ function KeySource() {
+ this.actions = new Map();
+ }
+
+ KeySource.prototype = {
+ serialize: function(tickCount) {
+ if (!this.actions.size) {
+ return undefined;
+ }
+ let actions = [];
+ let data = {"type": "key", "actions": actions};
+ for (let i=0; i<tickCount; i++) {
+ if (this.actions.has(i)) {
+ actions.push(this.actions.get(i));
+ } else {
+ actions.push({"type": "pause"});
+ }
+ }
+ return data;
+ },
+
+ keyDown: function(actions, key) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ tick = actions.addTick().tickIdx;
+ }
+ this.actions.set(tick, {type: "keyDown", value: key});
+ },
+
+ keyUp: function(actions, key) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ tick = actions.addTick().tickIdx;
+ }
+ this.actions.set(tick, {type: "keyUp", value: key});
+ },
+
+ addPause: function(actions, duration) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ tick = actions.addTick().tickIdx;
+ }
+ this.actions.set(tick, {type: "pause", duration: duration});
+ },
+ };
+
+ function PointerSource(parameters={pointerType: "mouse"}) {
+ let pointerType = parameters.pointerType || "mouse";
+ if (!["mouse", "pen", "touch"].includes(pointerType)) {
+ throw new Error(`Invalid pointerType ${pointerType}`);
+ }
+ this.type = pointerType;
+ this.actions = new Map();
+ }
+
+ function setPointerProperties(action, width, height, pressure, tangentialPressure,
+ tiltX, tiltY, twist, altitudeAngle, azimuthAngle) {
+ if (width) {
+ action.width = width;
+ }
+ if (height) {
+ action.height = height;
+ }
+ if (pressure) {
+ action.pressure = pressure;
+ }
+ if (tangentialPressure) {
+ action.tangentialPressure = tangentialPressure;
+ }
+ if (tiltX) {
+ action.tiltX = tiltX;
+ }
+ if (tiltY) {
+ action.tiltY = tiltY;
+ }
+ if (twist) {
+ action.twist = twist;
+ }
+ if (altitudeAngle) {
+ action.altitudeAngle = altitudeAngle;
+ }
+ if (azimuthAngle) {
+ action.azimuthAngle = azimuthAngle;
+ }
+ return action;
+ }
+
+ PointerSource.prototype = {
+ serialize: function(tickCount) {
+ if (!this.actions.size) {
+ return undefined;
+ }
+ let actions = [];
+ let data = {"type": "pointer", "actions": actions, "parameters": {"pointerType": this.type}};
+ for (let i=0; i<tickCount; i++) {
+ if (this.actions.has(i)) {
+ actions.push(this.actions.get(i));
+ } else {
+ actions.push({"type": "pause"});
+ }
+ }
+ return data;
+ },
+
+ pointerDown: function(actions, button, width, height, pressure, tangentialPressure,
+ tiltX, tiltY, twist, altitudeAngle, azimuthAngle) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ tick = actions.addTick().tickIdx;
+ }
+ let actionProperties = setPointerProperties({type: "pointerDown", button}, width, height,
+ pressure, tangentialPressure, tiltX, tiltY,
+ twist, altitudeAngle, azimuthAngle);
+ this.actions.set(tick, actionProperties);
+ },
+
+ pointerUp: function(actions, button) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ tick = actions.addTick().tickIdx;
+ }
+ this.actions.set(tick, {type: "pointerUp", button});
+ },
+
+ pointerMove: function(actions, x, y, duration, origin, width, height, pressure,
+ tangentialPressure, tiltX, tiltY, twist, altitudeAngle, azimuthAngle) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ tick = actions.addTick().tickIdx;
+ }
+ let moveAction = {type: "pointerMove", x, y, origin};
+ if (duration) {
+ moveAction.duration = duration;
+ }
+ let actionProperties = setPointerProperties(moveAction, width, height, pressure,
+ tangentialPressure, tiltX, tiltY, twist,
+ altitudeAngle, azimuthAngle);
+ this.actions.set(tick, actionProperties);
+ },
+
+ addPause: function(actions, duration) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ tick = actions.addTick().tickIdx;
+ }
+ this.actions.set(tick, {type: "pause", duration: duration});
+ },
+ };
+
+ function WheelSource() {
+ this.actions = new Map();
+ }
+
+ WheelSource.prototype = {
+ serialize: function(tickCount) {
+ if (!this.actions.size) {
+ return undefined;
+ }
+ let actions = [];
+ let data = {"type": "wheel", "actions": actions};
+ for (let i=0; i<tickCount; i++) {
+ if (this.actions.has(i)) {
+ actions.push(this.actions.get(i));
+ } else {
+ actions.push({"type": "pause"});
+ }
+ }
+ return data;
+ },
+
+ scroll: function(actions, x, y, deltaX, deltaY, duration, origin) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ tick = actions.addTick().tickIdx;
+ }
+ this.actions.set(tick, {type: "scroll", x, y, deltaX, deltaY, origin});
+ if (duration) {
+ this.actions.get(tick).duration = duration;
+ }
+ },
+
+ addPause: function(actions, duration) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ tick = actions.addTick().tickIdx;
+ }
+ this.actions.set(tick, {type: "pause", duration: duration});
+ },
+ };
+
+ test_driver.Actions = Actions;
+})();
diff --git a/testing/web-platform/tests/resources/testdriver-vendor.js b/testing/web-platform/tests/resources/testdriver-vendor.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/resources/testdriver-vendor.js
diff --git a/testing/web-platform/tests/resources/testdriver-vendor.js.headers b/testing/web-platform/tests/resources/testdriver-vendor.js.headers
new file mode 100644
index 0000000000..5e8f640c66
--- /dev/null
+++ b/testing/web-platform/tests/resources/testdriver-vendor.js.headers
@@ -0,0 +1,2 @@
+Content-Type: text/javascript; charset=utf-8
+Cache-Control: max-age=3600
diff --git a/testing/web-platform/tests/resources/testdriver.js b/testing/web-platform/tests/resources/testdriver.js
new file mode 100644
index 0000000000..446b033b0a
--- /dev/null
+++ b/testing/web-platform/tests/resources/testdriver.js
@@ -0,0 +1,809 @@
+(function() {
+ "use strict";
+ var idCounter = 0;
+ let testharness_context = null;
+
+ function getInViewCenterPoint(rect) {
+ var left = Math.max(0, rect.left);
+ var right = Math.min(window.innerWidth, rect.right);
+ var top = Math.max(0, rect.top);
+ var bottom = Math.min(window.innerHeight, rect.bottom);
+
+ var x = 0.5 * (left + right);
+ var y = 0.5 * (top + bottom);
+
+ return [x, y];
+ }
+
+ function getPointerInteractablePaintTree(element) {
+ let elementDocument = element.ownerDocument;
+ if (!elementDocument.contains(element)) {
+ return [];
+ }
+
+ var rectangles = element.getClientRects();
+
+ if (rectangles.length === 0) {
+ return [];
+ }
+
+ var centerPoint = getInViewCenterPoint(rectangles[0]);
+
+ if ("elementsFromPoint" in elementDocument) {
+ return elementDocument.elementsFromPoint(centerPoint[0], centerPoint[1]);
+ } else if ("msElementsFromPoint" in elementDocument) {
+ var rv = elementDocument.msElementsFromPoint(centerPoint[0], centerPoint[1]);
+ return Array.prototype.slice.call(rv ? rv : []);
+ } else {
+ throw new Error("document.elementsFromPoint unsupported");
+ }
+ }
+
+ function inView(element) {
+ var pointerInteractablePaintTree = getPointerInteractablePaintTree(element);
+ return pointerInteractablePaintTree.indexOf(element) !== -1;
+ }
+
+
+ /**
+ * @namespace {test_driver}
+ */
+ window.test_driver = {
+ /**
+ * Set the context in which testharness.js is loaded
+ *
+ * @param {WindowProxy} context - the window containing testharness.js
+ **/
+ set_test_context: function(context) {
+ if (window.test_driver_internal.set_test_context) {
+ window.test_driver_internal.set_test_context(context);
+ }
+ testharness_context = context;
+ },
+
+ /**
+ * postMessage to the context containing testharness.js
+ *
+ * @param {Object} msg - the data to POST
+ **/
+ message_test: function(msg) {
+ let target = testharness_context;
+ if (testharness_context === null) {
+ target = window;
+ }
+ target.postMessage(msg, "*");
+ },
+
+ /**
+ * Trigger user interaction in order to grant additional privileges to
+ * a provided function.
+ *
+ * See `Tracking user activation
+ * <https://html.spec.whatwg.org/multipage/interaction.html#tracking-user-activation>`_.
+ *
+ * @example
+ * var mediaElement = document.createElement('video');
+ *
+ * test_driver.bless('initiate media playback', function () {
+ * mediaElement.play();
+ * });
+ *
+ * @param {String} intent - a description of the action which must be
+ * triggered by user interaction
+ * @param {Function} action - code requiring escalated privileges
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled following user interaction and
+ * execution of the provided `action` function;
+ * rejected if interaction fails or the provided
+ * function throws an error
+ */
+ bless: function(intent, action, context=null) {
+ let contextDocument = context ? context.document : document;
+ var button = contextDocument.createElement("button");
+ button.innerHTML = "This test requires user interaction.<br />" +
+ "Please click here to allow " + intent + ".";
+ button.id = "wpt-test-driver-bless-" + (idCounter += 1);
+ const elem = contextDocument.body || contextDocument.documentElement;
+ elem.appendChild(button);
+
+ let wait_click = new Promise(resolve => button.addEventListener("click", resolve));
+
+ return test_driver.click(button)
+ .then(wait_click)
+ .then(function() {
+ button.remove();
+
+ if (typeof action === "function") {
+ return action();
+ }
+ return null;
+ });
+ },
+
+ /**
+ * Triggers a user-initiated click
+ *
+ * If ``element`` isn't inside the
+ * viewport, it will be scrolled into view before the click
+ * occurs.
+ *
+ * If ``element`` is from a different browsing context, the
+ * command will be run in that context.
+ *
+ * Matches the behaviour of the `Element Click
+ * <https://w3c.github.io/webdriver/#element-click>`_
+ * WebDriver command.
+ *
+ * **Note:** If the element to be clicked does not have a
+ * unique ID, the document must not have any DOM mutations
+ * made between the function being called and the promise
+ * settling.
+ *
+ * @param {Element} element - element to be clicked
+ * @returns {Promise} fulfilled after click occurs, or rejected in
+ * the cases the WebDriver command errors
+ */
+ click: function(element) {
+ if (!inView(element)) {
+ element.scrollIntoView({behavior: "instant",
+ block: "end",
+ inline: "nearest"});
+ }
+
+ var pointerInteractablePaintTree = getPointerInteractablePaintTree(element);
+ if (pointerInteractablePaintTree.length === 0 ||
+ !element.contains(pointerInteractablePaintTree[0])) {
+ return Promise.reject(new Error("element click intercepted error"));
+ }
+
+ var rect = element.getClientRects()[0];
+ var centerPoint = getInViewCenterPoint(rect);
+ return window.test_driver_internal.click(element,
+ {x: centerPoint[0],
+ y: centerPoint[1]});
+ },
+
+ /**
+ * Deletes all cookies.
+ *
+ * Matches the behaviour of the `Delete All Cookies
+ * <https://w3c.github.io/webdriver/#delete-all-cookies>`_
+ * WebDriver command.
+ *
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after cookies are deleted, or rejected in
+ * the cases the WebDriver command errors
+ */
+ delete_all_cookies: function(context=null) {
+ return window.test_driver_internal.delete_all_cookies(context);
+ },
+
+ /**
+ * Get details for all cookies in the current context.
+ * See https://w3c.github.io/webdriver/#get-all-cookies
+ *
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} Returns an array of cookies objects as defined in the spec:
+ * https://w3c.github.io/webdriver/#cookies
+ */
+ get_all_cookies: function(context=null) {
+ return window.test_driver_internal.get_all_cookies(context);
+ },
+
+ /**
+ * Get details for a cookie in the current context by name if it exists.
+ * See https://w3c.github.io/webdriver/#get-named-cookie
+ *
+ * @param {String} name - The name of the cookie to get.
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} Returns the matching cookie as defined in the spec:
+ * https://w3c.github.io/webdriver/#cookies
+ * Rejected if no such cookie exists.
+ */
+ get_named_cookie: async function(name, context=null) {
+ let cookie = await window.test_driver_internal.get_named_cookie(name, context);
+ if (!cookie) {
+ throw new Error("no such cookie");
+ }
+ return cookie;
+ },
+
+ /**
+ * Get Computed Label for an element.
+ *
+ * This matches the behaviour of the
+ * `Get Computed Label
+ * <https://w3c.github.io/webdriver/#dfn-get-computed-label>`_
+ * WebDriver command.
+ *
+ * @param {Element} element
+ * @returns {Promise} fulfilled after the computed label is returned, or
+ * rejected in the cases the WebDriver command errors
+ */
+ get_computed_label: async function(element) {
+ let label = await window.test_driver_internal.get_computed_label(element);
+ return label;
+ },
+
+ /**
+ * Get Computed Role for an element.
+ *
+ * This matches the behaviour of the
+ * `Get Computed Label
+ * <https://w3c.github.io/webdriver/#dfn-get-computed-role>`_
+ * WebDriver command.
+ *
+ * @param {Element} element
+ * @returns {Promise} fulfilled after the computed role is returned, or
+ * rejected in the cases the WebDriver command errors
+ */
+ get_computed_role: async function(element) {
+ let role = await window.test_driver_internal.get_computed_role(element);
+ return role;
+ },
+
+ /**
+ * Send keys to an element.
+ *
+ * If ``element`` isn't inside the
+ * viewport, it will be scrolled into view before the click
+ * occurs.
+ *
+ * If ``element`` is from a different browsing context, the
+ * command will be run in that context.
+ *
+ * To send special keys, send the respective key's codepoint,
+ * as defined by `WebDriver
+ * <https://w3c.github.io/webdriver/#keyboard-actions>`_. For
+ * example, the "tab" key is represented as "``\uE004``".
+ *
+ * **Note:** these special-key codepoints are not necessarily
+ * what you would expect. For example, <kbd>Esc</kbd> is the
+ * invalid Unicode character ``\uE00C``, not the ``\u001B`` Escape
+ * character from ASCII.
+ *
+ * This matches the behaviour of the
+ * `Send Keys
+ * <https://w3c.github.io/webdriver/#element-send-keys>`_
+ * WebDriver command.
+ *
+ * **Note:** If the element to be clicked does not have a
+ * unique ID, the document must not have any DOM mutations
+ * made between the function being called and the promise
+ * settling.
+ *
+ * @param {Element} element - element to send keys to
+ * @param {String} keys - keys to send to the element
+ * @returns {Promise} fulfilled after keys are sent, or rejected in
+ * the cases the WebDriver command errors
+ */
+ send_keys: function(element, keys) {
+ if (!inView(element)) {
+ element.scrollIntoView({behavior: "instant",
+ block: "end",
+ inline: "nearest"});
+ }
+
+ var pointerInteractablePaintTree = getPointerInteractablePaintTree(element);
+ if (pointerInteractablePaintTree.length === 0 ||
+ !element.contains(pointerInteractablePaintTree[0])) {
+ return Promise.reject(new Error("element send_keys intercepted error"));
+ }
+
+ return window.test_driver_internal.send_keys(element, keys);
+ },
+
+ /**
+ * Freeze the current page
+ *
+ * The freeze function transitions the page from the HIDDEN state to
+ * the FROZEN state as described in `Lifecycle API for Web Pages
+ * <https://github.com/WICG/page-lifecycle/blob/master/README.md>`_.
+ *
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the freeze request is sent, or rejected
+ * in case the WebDriver command errors
+ */
+ freeze: function(context=null) {
+ return window.test_driver_internal.freeze();
+ },
+
+ /**
+ * Minimizes the browser window.
+ *
+ * Matches the the behaviour of the `Minimize
+ * <https://www.w3.org/TR/webdriver/#minimize-window>`_
+ * WebDriver command
+ *
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled with the previous {@link
+ * https://www.w3.org/TR/webdriver/#dfn-windowrect-object|WindowRect}
+ * value, after the window is minimized.
+ */
+ minimize_window: function(context=null) {
+ return window.test_driver_internal.minimize_window(context);
+ },
+
+ /**
+ * Restore the window from minimized/maximized state to a given rect.
+ *
+ * Matches the behaviour of the `Set Window Rect
+ * <https://www.w3.org/TR/webdriver/#set-window-rect>`_
+ * WebDriver command
+ *
+ * @param {Object} rect - A {@link
+ * https://www.w3.org/TR/webdriver/#dfn-windowrect-object|WindowRect}
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the window is restored to the given rect.
+ */
+ set_window_rect: function(rect, context=null) {
+ return window.test_driver_internal.set_window_rect(rect, context);
+ },
+
+ /**
+ * Send a sequence of actions
+ *
+ * This function sends a sequence of actions to perform.
+ *
+ * Matches the behaviour of the `Actions
+ * <https://w3c.github.io/webdriver/#actions>`_ feature in
+ * WebDriver.
+ *
+ * Authors are encouraged to use the
+ * :js:class:`test_driver.Actions` builder rather than
+ * invoking this API directly.
+ *
+ * @param {Array} actions - an array of actions. The format is
+ * the same as the actions property
+ * of the `Perform Actions
+ * <https://w3c.github.io/webdriver/#perform-actions>`_
+ * WebDriver command. Each element is
+ * an object representing an input
+ * source and each input source
+ * itself has an actions property
+ * detailing the behaviour of that
+ * source at each timestep (or
+ * tick). Authors are not expected to
+ * construct the actions sequence by
+ * hand, but to use the builder api
+ * provided in testdriver-actions.js
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the actions are performed, or rejected in
+ * the cases the WebDriver command errors
+ */
+ action_sequence: function(actions, context=null) {
+ return window.test_driver_internal.action_sequence(actions, context);
+ },
+
+ /**
+ * Generates a test report on the current page
+ *
+ * The generate_test_report function generates a report (to be
+ * observed by ReportingObserver) for testing purposes.
+ *
+ * Matches the `Generate Test Report
+ * <https://w3c.github.io/reporting/#generate-test-report-command>`_
+ * WebDriver command.
+ *
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the report is generated, or
+ * rejected if the report generation fails
+ */
+ generate_test_report: function(message, context=null) {
+ return window.test_driver_internal.generate_test_report(message, context);
+ },
+
+ /**
+ * Sets the state of a permission
+ *
+ * This function simulates a user setting a permission into a
+ * particular state.
+ *
+ * Matches the `Set Permission
+ * <https://w3c.github.io/permissions/#set-permission-command>`_
+ * WebDriver command.
+ *
+ * @example
+ * await test_driver.set_permission({ name: "background-fetch" }, "denied");
+ * await test_driver.set_permission({ name: "push", userVisibleOnly: true }, "granted");
+ *
+ * @param {PermissionDescriptor} descriptor - a `PermissionDescriptor
+ * <https://w3c.github.io/permissions/#dom-permissiondescriptor>`_
+ * dictionary.
+ * @param {String} state - the state of the permission
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ * @returns {Promise} fulfilled after the permission is set, or rejected if setting the
+ * permission fails
+ */
+ set_permission: function(descriptor, state, context=null) {
+ let permission_params = {
+ descriptor,
+ state,
+ };
+ return window.test_driver_internal.set_permission(permission_params, context);
+ },
+
+ /**
+ * Creates a virtual authenticator
+ *
+ * This function creates a virtual authenticator for use with
+ * the U2F and WebAuthn APIs.
+ *
+ * Matches the `Add Virtual Authenticator
+ * <https://w3c.github.io/webauthn/#sctn-automation-add-virtual-authenticator>`_
+ * WebDriver command.
+ *
+ * @param {Object} config - an `Authenticator Configuration
+ * <https://w3c.github.io/webauthn/#authenticator-configuration>`_
+ * object
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the authenticator is added, or
+ * rejected in the cases the WebDriver command
+ * errors. Returns the ID of the authenticator
+ */
+ add_virtual_authenticator: function(config, context=null) {
+ return window.test_driver_internal.add_virtual_authenticator(config, context);
+ },
+
+ /**
+ * Removes a virtual authenticator
+ *
+ * This function removes a virtual authenticator that has been
+ * created by :js:func:`add_virtual_authenticator`.
+ *
+ * Matches the `Remove Virtual Authenticator
+ * <https://w3c.github.io/webauthn/#sctn-automation-remove-virtual-authenticator>`_
+ * WebDriver command.
+ *
+ * @param {String} authenticator_id - the ID of the authenticator to be
+ * removed.
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the authenticator is removed, or
+ * rejected in the cases the WebDriver command
+ * errors
+ */
+ remove_virtual_authenticator: function(authenticator_id, context=null) {
+ return window.test_driver_internal.remove_virtual_authenticator(authenticator_id, context);
+ },
+
+ /**
+ * Adds a credential to a virtual authenticator
+ *
+ * Matches the `Add Credential
+ * <https://w3c.github.io/webauthn/#sctn-automation-add-credential>`_
+ * WebDriver command.
+ *
+ * @param {String} authenticator_id - the ID of the authenticator
+ * @param {Object} credential - A `Credential Parameters
+ * <https://w3c.github.io/webauthn/#credential-parameters>`_
+ * object
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the credential is added, or
+ * rejected in the cases the WebDriver command
+ * errors
+ */
+ add_credential: function(authenticator_id, credential, context=null) {
+ return window.test_driver_internal.add_credential(authenticator_id, credential, context);
+ },
+
+ /**
+ * Gets all the credentials stored in an authenticator
+ *
+ * This function retrieves all the credentials (added via the U2F API,
+ * WebAuthn, or the add_credential function) stored in a virtual
+ * authenticator
+ *
+ * Matches the `Get Credentials
+ * <https://w3c.github.io/webauthn/#sctn-automation-get-credentials>`_
+ * WebDriver command.
+ *
+ * @param {String} authenticator_id - the ID of the authenticator
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the credentials are
+ * returned, or rejected in the cases the
+ * WebDriver command errors. Returns an
+ * array of `Credential Parameters
+ * <https://w3c.github.io/webauthn/#credential-parameters>`_
+ */
+ get_credentials: function(authenticator_id, context=null) {
+ return window.test_driver_internal.get_credentials(authenticator_id, context=null);
+ },
+
+ /**
+ * Remove a credential stored in an authenticator
+ *
+ * Matches the `Remove Credential
+ * <https://w3c.github.io/webauthn/#sctn-automation-remove-credential>`_
+ * WebDriver command.
+ *
+ * @param {String} authenticator_id - the ID of the authenticator
+ * @param {String} credential_id - the ID of the credential
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the credential is removed, or
+ * rejected in the cases the WebDriver command
+ * errors.
+ */
+ remove_credential: function(authenticator_id, credential_id, context=null) {
+ return window.test_driver_internal.remove_credential(authenticator_id, credential_id, context);
+ },
+
+ /**
+ * Removes all the credentials stored in a virtual authenticator
+ *
+ * Matches the `Remove All Credentials
+ * <https://w3c.github.io/webauthn/#sctn-automation-remove-all-credentials>`_
+ * WebDriver command.
+ *
+ * @param {String} authenticator_id - the ID of the authenticator
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the credentials are removed, or
+ * rejected in the cases the WebDriver command
+ * errors.
+ */
+ remove_all_credentials: function(authenticator_id, context=null) {
+ return window.test_driver_internal.remove_all_credentials(authenticator_id, context);
+ },
+
+ /**
+ * Sets the User Verified flag on an authenticator
+ *
+ * Sets whether requests requiring user verification will succeed or
+ * fail on a given virtual authenticator
+ *
+ * Matches the `Set User Verified
+ * <https://w3c.github.io/webauthn/#sctn-automation-set-user-verified>`_
+ * WebDriver command.
+ *
+ * @param {String} authenticator_id - the ID of the authenticator
+ * @param {boolean} uv - the User Verified flag
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ */
+ set_user_verified: function(authenticator_id, uv, context=null) {
+ return window.test_driver_internal.set_user_verified(authenticator_id, uv, context);
+ },
+
+ /**
+ * Sets the storage access rule for an origin when embedded
+ * in a third-party context.
+ *
+ * Matches the `Set Storage Access
+ * <https://privacycg.github.io/storage-access/#set-storage-access-command>`_
+ * WebDriver command.
+ *
+ * @param {String} origin - A third-party origin to block or allow.
+ * May be "*" to indicate all origins.
+ * @param {String} embedding_origin - an embedding (first-party) origin
+ * on which {origin}'s access should
+ * be blocked or allowed.
+ * May be "*" to indicate all origins.
+ * @param {String} state - The storage access setting.
+ * Must be either "allowed" or "blocked".
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} Fulfilled after the storage access rule has been
+ * set, or rejected if setting the rule fails.
+ */
+ set_storage_access: function(origin, embedding_origin, state, context=null) {
+ if (state !== "allowed" && state !== "blocked") {
+ throw new Error("storage access status must be 'allowed' or 'blocked'");
+ }
+ const blocked = state === "blocked";
+ return window.test_driver_internal.set_storage_access(origin, embedding_origin, blocked, context);
+ },
+
+ /**
+ * Sets the current transaction automation mode for Secure Payment
+ * Confirmation.
+ *
+ * This function places `Secure Payment
+ * Confirmation <https://w3c.github.io/secure-payment-confirmation>`_ into
+ * an automated 'autoaccept' or 'autoreject' mode, to allow testing
+ * without user interaction with the transaction UX prompt.
+ *
+ * Matches the `Set SPC Transaction Mode
+ * <https://w3c.github.io/secure-payment-confirmation/#sctn-automation-set-spc-transaction-mode>`_
+ * WebDriver command.
+ *
+ * @example
+ * await test_driver.set_spc_transaction_mode("autoaccept");
+ * test.add_cleanup(() => {
+ * return test_driver.set_spc_transaction_mode("none");
+ * });
+ *
+ * // Assumption: `request` is a PaymentRequest with a secure-payment-confirmation
+ * // payment method.
+ * const response = await request.show();
+ *
+ * @param {String} mode - The `transaction mode
+ * <https://w3c.github.io/secure-payment-confirmation/#enumdef-transactionautomationmode>`_
+ * to set. Must be one of "``none``",
+ * "``autoaccept``", or
+ * "``autoreject``".
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} Fulfilled after the transaction mode has been set,
+ * or rejected if setting the mode fails.
+ */
+ set_spc_transaction_mode: function(mode, context=null) {
+ return window.test_driver_internal.set_spc_transaction_mode(mode, context);
+ },
+ };
+
+ window.test_driver_internal = {
+ /**
+ * This flag should be set to `true` by any code which implements the
+ * internal methods defined below for automation purposes. Doing so
+ * allows the library to signal failure immediately when an automated
+ * implementation of one of the methods is not available.
+ */
+ in_automation: false,
+
+ async click(element, coords) {
+ if (this.in_automation) {
+ throw new Error("click() is not implemented by testdriver-vendor.js");
+ }
+
+ return new Promise(function(resolve, reject) {
+ element.addEventListener("click", resolve);
+ });
+ },
+
+ async delete_all_cookies(context=null) {
+ throw new Error("delete_all_cookies() is not implemented by testdriver-vendor.js");
+ },
+
+ async get_all_cookies(context=null) {
+ throw new Error("get_all_cookies() is not implemented by testdriver-vendor.js");
+ },
+
+ async get_named_cookie(name, context=null) {
+ throw new Error("get_named_cookie() is not implemented by testdriver-vendor.js");
+ },
+
+ async send_keys(element, keys) {
+ if (this.in_automation) {
+ throw new Error("send_keys() is not implemented by testdriver-vendor.js");
+ }
+
+ return new Promise(function(resolve, reject) {
+ var seen = "";
+
+ function remove() {
+ element.removeEventListener("keydown", onKeyDown);
+ }
+
+ function onKeyDown(event) {
+ if (event.key.length > 1) {
+ return;
+ }
+
+ seen += event.key;
+
+ if (keys.indexOf(seen) !== 0) {
+ reject(new Error("Unexpected key sequence: " + seen));
+ remove();
+ } else if (seen === keys) {
+ resolve();
+ remove();
+ }
+ }
+
+ element.addEventListener("keydown", onKeyDown);
+ });
+ },
+
+ async freeze(context=null) {
+ throw new Error("freeze() is not implemented by testdriver-vendor.js");
+ },
+
+ async minimize_window(context=null) {
+ throw new Error("minimize_window() is not implemented by testdriver-vendor.js");
+ },
+
+ async set_window_rect(rect, context=null) {
+ throw new Error("set_window_rect() is not implemented by testdriver-vendor.js");
+ },
+
+ async action_sequence(actions, context=null) {
+ throw new Error("action_sequence() is not implemented by testdriver-vendor.js");
+ },
+
+ async generate_test_report(message, context=null) {
+ throw new Error("generate_test_report() is not implemented by testdriver-vendor.js");
+ },
+
+ async set_permission(permission_params, context=null) {
+ throw new Error("set_permission() is not implemented by testdriver-vendor.js");
+ },
+
+ async add_virtual_authenticator(config, context=null) {
+ throw new Error("add_virtual_authenticator() is not implemented by testdriver-vendor.js");
+ },
+
+ async remove_virtual_authenticator(authenticator_id, context=null) {
+ throw new Error("remove_virtual_authenticator() is not implemented by testdriver-vendor.js");
+ },
+
+ async add_credential(authenticator_id, credential, context=null) {
+ throw new Error("add_credential() is not implemented by testdriver-vendor.js");
+ },
+
+ async get_credentials(authenticator_id, context=null) {
+ throw new Error("get_credentials() is not implemented by testdriver-vendor.js");
+ },
+
+ async remove_credential(authenticator_id, credential_id, context=null) {
+ throw new Error("remove_credential() is not implemented by testdriver-vendor.js");
+ },
+
+ async remove_all_credentials(authenticator_id, context=null) {
+ throw new Error("remove_all_credentials() is not implemented by testdriver-vendor.js");
+ },
+
+ async set_user_verified(authenticator_id, uv, context=null) {
+ throw new Error("set_user_verified() is not implemented by testdriver-vendor.js");
+ },
+
+ async set_storage_access(origin, embedding_origin, blocked, context=null) {
+ throw new Error("set_storage_access() is not implemented by testdriver-vendor.js");
+ },
+
+ async set_spc_transaction_mode(mode, context=null) {
+ throw new Error("set_spc_transaction_mode() is not implemented by testdriver-vendor.js");
+ },
+
+ };
+})();
diff --git a/testing/web-platform/tests/resources/testdriver.js.headers b/testing/web-platform/tests/resources/testdriver.js.headers
new file mode 100644
index 0000000000..5e8f640c66
--- /dev/null
+++ b/testing/web-platform/tests/resources/testdriver.js.headers
@@ -0,0 +1,2 @@
+Content-Type: text/javascript; charset=utf-8
+Cache-Control: max-age=3600
diff --git a/testing/web-platform/tests/resources/testharness.js b/testing/web-platform/tests/resources/testharness.js
new file mode 100644
index 0000000000..413993089b
--- /dev/null
+++ b/testing/web-platform/tests/resources/testharness.js
@@ -0,0 +1,4933 @@
+/*global self*/
+/*jshint latedef: nofunc*/
+
+/* Documentation: https://web-platform-tests.org/writing-tests/testharness-api.html
+ * (../docs/_writing-tests/testharness-api.md) */
+
+(function (global_scope)
+{
+ // default timeout is 10 seconds, test can override if needed
+ var settings = {
+ output:true,
+ harness_timeout:{
+ "normal":10000,
+ "long":60000
+ },
+ test_timeout:null,
+ message_events: ["start", "test_state", "result", "completion"],
+ debug: false,
+ };
+
+ var xhtml_ns = "http://www.w3.org/1999/xhtml";
+
+ /*
+ * TestEnvironment is an abstraction for the environment in which the test
+ * harness is used. Each implementation of a test environment has to provide
+ * the following interface:
+ *
+ * interface TestEnvironment {
+ * // Invoked after the global 'tests' object has been created and it's
+ * // safe to call add_*_callback() to register event handlers.
+ * void on_tests_ready();
+ *
+ * // Invoked after setup() has been called to notify the test environment
+ * // of changes to the test harness properties.
+ * void on_new_harness_properties(object properties);
+ *
+ * // Should return a new unique default test name.
+ * DOMString next_default_test_name();
+ *
+ * // Should return the test harness timeout duration in milliseconds.
+ * float test_timeout();
+ * };
+ */
+
+ /*
+ * A test environment with a DOM. The global object is 'window'. By default
+ * test results are displayed in a table. Any parent windows receive
+ * callbacks or messages via postMessage() when test events occur. See
+ * apisample11.html and apisample12.html.
+ */
+ function WindowTestEnvironment() {
+ this.name_counter = 0;
+ this.window_cache = null;
+ this.output_handler = null;
+ this.all_loaded = false;
+ var this_obj = this;
+ this.message_events = [];
+ this.dispatched_messages = [];
+
+ this.message_functions = {
+ start: [add_start_callback, remove_start_callback,
+ function (properties) {
+ this_obj._dispatch("start_callback", [properties],
+ {type: "start", properties: properties});
+ }],
+
+ test_state: [add_test_state_callback, remove_test_state_callback,
+ function(test) {
+ this_obj._dispatch("test_state_callback", [test],
+ {type: "test_state",
+ test: test.structured_clone()});
+ }],
+ result: [add_result_callback, remove_result_callback,
+ function (test) {
+ this_obj.output_handler.show_status();
+ this_obj._dispatch("result_callback", [test],
+ {type: "result",
+ test: test.structured_clone()});
+ }],
+ completion: [add_completion_callback, remove_completion_callback,
+ function (tests, harness_status, asserts) {
+ var cloned_tests = map(tests, function(test) {
+ return test.structured_clone();
+ });
+ this_obj._dispatch("completion_callback", [tests, harness_status],
+ {type: "complete",
+ tests: cloned_tests,
+ status: harness_status.structured_clone(),
+ asserts: asserts.map(assert => assert.structured_clone())});
+ }]
+ }
+
+ on_event(window, 'load', function() {
+ this_obj.all_loaded = true;
+ });
+
+ on_event(window, 'message', function(event) {
+ if (event.data && event.data.type === "getmessages" && event.source) {
+ // A window can post "getmessages" to receive a duplicate of every
+ // message posted by this environment so far. This allows subscribers
+ // from fetch_tests_from_window to 'catch up' to the current state of
+ // this environment.
+ for (var i = 0; i < this_obj.dispatched_messages.length; ++i)
+ {
+ event.source.postMessage(this_obj.dispatched_messages[i], "*");
+ }
+ }
+ });
+ }
+
+ WindowTestEnvironment.prototype._dispatch = function(selector, callback_args, message_arg) {
+ this.dispatched_messages.push(message_arg);
+ this._forEach_windows(
+ function(w, same_origin) {
+ if (same_origin) {
+ try {
+ var has_selector = selector in w;
+ } catch(e) {
+ // If document.domain was set at some point same_origin can be
+ // wrong and the above will fail.
+ has_selector = false;
+ }
+ if (has_selector) {
+ try {
+ w[selector].apply(undefined, callback_args);
+ } catch (e) {}
+ }
+ }
+ if (w !== self) {
+ w.postMessage(message_arg, "*");
+ }
+ });
+ };
+
+ WindowTestEnvironment.prototype._forEach_windows = function(callback) {
+ // Iterate over the windows [self ... top, opener]. The callback is passed
+ // two objects, the first one is the window object itself, the second one
+ // is a boolean indicating whether or not it's on the same origin as the
+ // current window.
+ var cache = this.window_cache;
+ if (!cache) {
+ cache = [[self, true]];
+ var w = self;
+ var i = 0;
+ var so;
+ while (w != w.parent) {
+ w = w.parent;
+ so = is_same_origin(w);
+ cache.push([w, so]);
+ i++;
+ }
+ w = window.opener;
+ if (w) {
+ cache.push([w, is_same_origin(w)]);
+ }
+ this.window_cache = cache;
+ }
+
+ forEach(cache,
+ function(a) {
+ callback.apply(null, a);
+ });
+ };
+
+ WindowTestEnvironment.prototype.on_tests_ready = function() {
+ var output = new Output();
+ this.output_handler = output;
+
+ var this_obj = this;
+
+ add_start_callback(function (properties) {
+ this_obj.output_handler.init(properties);
+ });
+
+ add_test_state_callback(function(test) {
+ this_obj.output_handler.show_status();
+ });
+
+ add_result_callback(function (test) {
+ this_obj.output_handler.show_status();
+ });
+
+ add_completion_callback(function (tests, harness_status, asserts_run) {
+ this_obj.output_handler.show_results(tests, harness_status, asserts_run);
+ });
+ this.setup_messages(settings.message_events);
+ };
+
+ WindowTestEnvironment.prototype.setup_messages = function(new_events) {
+ var this_obj = this;
+ forEach(settings.message_events, function(x) {
+ var current_dispatch = this_obj.message_events.indexOf(x) !== -1;
+ var new_dispatch = new_events.indexOf(x) !== -1;
+ if (!current_dispatch && new_dispatch) {
+ this_obj.message_functions[x][0](this_obj.message_functions[x][2]);
+ } else if (current_dispatch && !new_dispatch) {
+ this_obj.message_functions[x][1](this_obj.message_functions[x][2]);
+ }
+ });
+ this.message_events = new_events;
+ }
+
+ WindowTestEnvironment.prototype.next_default_test_name = function() {
+ var suffix = this.name_counter > 0 ? " " + this.name_counter : "";
+ this.name_counter++;
+ return get_title() + suffix;
+ };
+
+ WindowTestEnvironment.prototype.on_new_harness_properties = function(properties) {
+ this.output_handler.setup(properties);
+ if (properties.hasOwnProperty("message_events")) {
+ this.setup_messages(properties.message_events);
+ }
+ };
+
+ WindowTestEnvironment.prototype.add_on_loaded_callback = function(callback) {
+ on_event(window, 'load', callback);
+ };
+
+ WindowTestEnvironment.prototype.test_timeout = function() {
+ var metas = document.getElementsByTagName("meta");
+ for (var i = 0; i < metas.length; i++) {
+ if (metas[i].name == "timeout") {
+ if (metas[i].content == "long") {
+ return settings.harness_timeout.long;
+ }
+ break;
+ }
+ }
+ return settings.harness_timeout.normal;
+ };
+
+ /*
+ * Base TestEnvironment implementation for a generic web worker.
+ *
+ * Workers accumulate test results. One or more clients can connect and
+ * retrieve results from a worker at any time.
+ *
+ * WorkerTestEnvironment supports communicating with a client via a
+ * MessagePort. The mechanism for determining the appropriate MessagePort
+ * for communicating with a client depends on the type of worker and is
+ * implemented by the various specializations of WorkerTestEnvironment
+ * below.
+ *
+ * A client document using testharness can use fetch_tests_from_worker() to
+ * retrieve results from a worker. See apisample16.html.
+ */
+ function WorkerTestEnvironment() {
+ this.name_counter = 0;
+ this.all_loaded = true;
+ this.message_list = [];
+ this.message_ports = [];
+ }
+
+ WorkerTestEnvironment.prototype._dispatch = function(message) {
+ this.message_list.push(message);
+ for (var i = 0; i < this.message_ports.length; ++i)
+ {
+ this.message_ports[i].postMessage(message);
+ }
+ };
+
+ // The only requirement is that port has a postMessage() method. It doesn't
+ // have to be an instance of a MessagePort, and often isn't.
+ WorkerTestEnvironment.prototype._add_message_port = function(port) {
+ this.message_ports.push(port);
+ for (var i = 0; i < this.message_list.length; ++i)
+ {
+ port.postMessage(this.message_list[i]);
+ }
+ };
+
+ WorkerTestEnvironment.prototype.next_default_test_name = function() {
+ var suffix = this.name_counter > 0 ? " " + this.name_counter : "";
+ this.name_counter++;
+ return get_title() + suffix;
+ };
+
+ WorkerTestEnvironment.prototype.on_new_harness_properties = function() {};
+
+ WorkerTestEnvironment.prototype.on_tests_ready = function() {
+ var this_obj = this;
+ add_start_callback(
+ function(properties) {
+ this_obj._dispatch({
+ type: "start",
+ properties: properties,
+ });
+ });
+ add_test_state_callback(
+ function(test) {
+ this_obj._dispatch({
+ type: "test_state",
+ test: test.structured_clone()
+ });
+ });
+ add_result_callback(
+ function(test) {
+ this_obj._dispatch({
+ type: "result",
+ test: test.structured_clone()
+ });
+ });
+ add_completion_callback(
+ function(tests, harness_status, asserts) {
+ this_obj._dispatch({
+ type: "complete",
+ tests: map(tests,
+ function(test) {
+ return test.structured_clone();
+ }),
+ status: harness_status.structured_clone(),
+ asserts: asserts.map(assert => assert.structured_clone()),
+ });
+ });
+ };
+
+ WorkerTestEnvironment.prototype.add_on_loaded_callback = function() {};
+
+ WorkerTestEnvironment.prototype.test_timeout = function() {
+ // Tests running in a worker don't have a default timeout. I.e. all
+ // worker tests behave as if settings.explicit_timeout is true.
+ return null;
+ };
+
+ /*
+ * Dedicated web workers.
+ * https://html.spec.whatwg.org/multipage/workers.html#dedicatedworkerglobalscope
+ *
+ * This class is used as the test_environment when testharness is running
+ * inside a dedicated worker.
+ */
+ function DedicatedWorkerTestEnvironment() {
+ WorkerTestEnvironment.call(this);
+ // self is an instance of DedicatedWorkerGlobalScope which exposes
+ // a postMessage() method for communicating via the message channel
+ // established when the worker is created.
+ this._add_message_port(self);
+ }
+ DedicatedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype);
+
+ DedicatedWorkerTestEnvironment.prototype.on_tests_ready = function() {
+ WorkerTestEnvironment.prototype.on_tests_ready.call(this);
+ // In the absence of an onload notification, we a require dedicated
+ // workers to explicitly signal when the tests are done.
+ tests.wait_for_finish = true;
+ };
+
+ /*
+ * Shared web workers.
+ * https://html.spec.whatwg.org/multipage/workers.html#sharedworkerglobalscope
+ *
+ * This class is used as the test_environment when testharness is running
+ * inside a shared web worker.
+ */
+ function SharedWorkerTestEnvironment() {
+ WorkerTestEnvironment.call(this);
+ var this_obj = this;
+ // Shared workers receive message ports via the 'onconnect' event for
+ // each connection.
+ self.addEventListener("connect",
+ function(message_event) {
+ this_obj._add_message_port(message_event.source);
+ }, false);
+ }
+ SharedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype);
+
+ SharedWorkerTestEnvironment.prototype.on_tests_ready = function() {
+ WorkerTestEnvironment.prototype.on_tests_ready.call(this);
+ // In the absence of an onload notification, we a require shared
+ // workers to explicitly signal when the tests are done.
+ tests.wait_for_finish = true;
+ };
+
+ /*
+ * Service workers.
+ * http://www.w3.org/TR/service-workers/
+ *
+ * This class is used as the test_environment when testharness is running
+ * inside a service worker.
+ */
+ function ServiceWorkerTestEnvironment() {
+ WorkerTestEnvironment.call(this);
+ this.all_loaded = false;
+ this.on_loaded_callback = null;
+ var this_obj = this;
+ self.addEventListener("message",
+ function(event) {
+ if (event.data && event.data.type && event.data.type === "connect") {
+ this_obj._add_message_port(event.source);
+ }
+ }, false);
+
+ // The oninstall event is received after the service worker script and
+ // all imported scripts have been fetched and executed. It's the
+ // equivalent of an onload event for a document. All tests should have
+ // been added by the time this event is received, thus it's not
+ // necessary to wait until the onactivate event. However, tests for
+ // installed service workers need another event which is equivalent to
+ // the onload event because oninstall is fired only on installation. The
+ // onmessage event is used for that purpose since tests using
+ // testharness.js should ask the result to its service worker by
+ // PostMessage. If the onmessage event is triggered on the service
+ // worker's context, that means the worker's script has been evaluated.
+ on_event(self, "install", on_all_loaded);
+ on_event(self, "message", on_all_loaded);
+ function on_all_loaded() {
+ if (this_obj.all_loaded)
+ return;
+ this_obj.all_loaded = true;
+ if (this_obj.on_loaded_callback) {
+ this_obj.on_loaded_callback();
+ }
+ }
+ }
+
+ ServiceWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype);
+
+ ServiceWorkerTestEnvironment.prototype.add_on_loaded_callback = function(callback) {
+ if (this.all_loaded) {
+ callback();
+ } else {
+ this.on_loaded_callback = callback;
+ }
+ };
+
+ /*
+ * Shadow realms.
+ * https://github.com/tc39/proposal-shadowrealm
+ *
+ * This class is used as the test_environment when testharness is running
+ * inside a shadow realm.
+ */
+ function ShadowRealmTestEnvironment() {
+ WorkerTestEnvironment.call(this);
+ this.all_loaded = false;
+ this.on_loaded_callback = null;
+ }
+
+ ShadowRealmTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype);
+
+ /**
+ * Signal to the test environment that the tests are ready and the on-loaded
+ * callback should be run.
+ *
+ * Shadow realms are not *really* a DOM context: they have no `onload` or similar
+ * event for us to use to set up the test environment; so, instead, this method
+ * is manually triggered from the incubating realm
+ *
+ * @param {Function} message_destination - a function that receives JSON-serializable
+ * data to send to the incubating realm, in the same format as used by RemoteContext
+ */
+ ShadowRealmTestEnvironment.prototype.begin = function(message_destination) {
+ if (this.all_loaded) {
+ throw new Error("Tried to start a shadow realm test environment after it has already started");
+ }
+ var fakeMessagePort = {};
+ fakeMessagePort.postMessage = message_destination;
+ this._add_message_port(fakeMessagePort);
+ this.all_loaded = true;
+ if (this.on_loaded_callback) {
+ this.on_loaded_callback();
+ }
+ };
+
+ ShadowRealmTestEnvironment.prototype.add_on_loaded_callback = function(callback) {
+ if (this.all_loaded) {
+ callback();
+ } else {
+ this.on_loaded_callback = callback;
+ }
+ };
+
+ /*
+ * JavaScript shells.
+ *
+ * This class is used as the test_environment when testharness is running
+ * inside a JavaScript shell.
+ */
+ function ShellTestEnvironment() {
+ this.name_counter = 0;
+ this.all_loaded = false;
+ this.on_loaded_callback = null;
+ Promise.resolve().then(function() {
+ this.all_loaded = true
+ if (this.on_loaded_callback) {
+ this.on_loaded_callback();
+ }
+ }.bind(this));
+ this.message_list = [];
+ this.message_ports = [];
+ }
+
+ ShellTestEnvironment.prototype.next_default_test_name = function() {
+ var suffix = this.name_counter > 0 ? " " + this.name_counter : "";
+ this.name_counter++;
+ return get_title() + suffix;
+ };
+
+ ShellTestEnvironment.prototype.on_new_harness_properties = function() {};
+
+ ShellTestEnvironment.prototype.on_tests_ready = function() {};
+
+ ShellTestEnvironment.prototype.add_on_loaded_callback = function(callback) {
+ if (this.all_loaded) {
+ callback();
+ } else {
+ this.on_loaded_callback = callback;
+ }
+ };
+
+ ShellTestEnvironment.prototype.test_timeout = function() {
+ // Tests running in a shell don't have a default timeout, so behave as
+ // if settings.explicit_timeout is true.
+ return null;
+ };
+
+ function create_test_environment() {
+ if ('document' in global_scope) {
+ return new WindowTestEnvironment();
+ }
+ if ('DedicatedWorkerGlobalScope' in global_scope &&
+ global_scope instanceof DedicatedWorkerGlobalScope) {
+ return new DedicatedWorkerTestEnvironment();
+ }
+ if ('SharedWorkerGlobalScope' in global_scope &&
+ global_scope instanceof SharedWorkerGlobalScope) {
+ return new SharedWorkerTestEnvironment();
+ }
+ if ('ServiceWorkerGlobalScope' in global_scope &&
+ global_scope instanceof ServiceWorkerGlobalScope) {
+ return new ServiceWorkerTestEnvironment();
+ }
+ if ('WorkerGlobalScope' in global_scope &&
+ global_scope instanceof WorkerGlobalScope) {
+ return new DedicatedWorkerTestEnvironment();
+ }
+ /* Shadow realm global objects are _ordinary_ objects (i.e. their prototype is
+ * Object) so we don't have a nice `instanceof` test to use; instead, we
+ * check if the there is a GLOBAL.isShadowRealm() property
+ * on the global object. that was set by the test harness when it
+ * created the ShadowRealm.
+ */
+ if (global_scope.GLOBAL && global_scope.GLOBAL.isShadowRealm()) {
+ return new ShadowRealmTestEnvironment();
+ }
+
+ return new ShellTestEnvironment();
+ }
+
+ var test_environment = create_test_environment();
+
+ function is_shared_worker(worker) {
+ return 'SharedWorker' in global_scope && worker instanceof SharedWorker;
+ }
+
+ function is_service_worker(worker) {
+ // The worker object may be from another execution context,
+ // so do not use instanceof here.
+ return 'ServiceWorker' in global_scope &&
+ Object.prototype.toString.call(worker) == '[object ServiceWorker]';
+ }
+
+ var seen_func_name = Object.create(null);
+
+ function get_test_name(func, name)
+ {
+ if (name) {
+ return name;
+ }
+
+ if (func) {
+ var func_code = func.toString();
+
+ // Try and match with brackets, but fallback to matching without
+ var arrow = func_code.match(/^\(\)\s*=>\s*(?:{(.*)}\s*|(.*))$/);
+
+ // Check for JS line separators
+ if (arrow !== null && !/[\u000A\u000D\u2028\u2029]/.test(func_code)) {
+ var trimmed = (arrow[1] !== undefined ? arrow[1] : arrow[2]).trim();
+ // drop trailing ; if there's no earlier ones
+ trimmed = trimmed.replace(/^([^;]*)(;\s*)+$/, "$1");
+
+ if (trimmed) {
+ let name = trimmed;
+ if (seen_func_name[trimmed]) {
+ // This subtest name already exists, so add a suffix.
+ name += " " + seen_func_name[trimmed];
+ } else {
+ seen_func_name[trimmed] = 0;
+ }
+ seen_func_name[trimmed] += 1;
+ return name;
+ }
+ }
+ }
+
+ return test_environment.next_default_test_name();
+ }
+
+ /**
+ * @callback TestFunction
+ * @param {Test} test - The test currnetly being run.
+ * @param {Any[]} args - Additional args to pass to function.
+ *
+ */
+
+ /**
+ * Create a synchronous test
+ *
+ * @param {TestFunction} func - Test function. This is executed
+ * immediately. If it returns without error, the test status is
+ * set to ``PASS``. If it throws an :js:class:`AssertionError`, or
+ * any other exception, the test status is set to ``FAIL``
+ * (typically from an `assert` function).
+ * @param {String} name - Test name. This must be unique in a
+ * given file and must be invariant between runs.
+ */
+ function test(func, name, properties)
+ {
+ if (tests.promise_setup_called) {
+ tests.status.status = tests.status.ERROR;
+ tests.status.message = '`test` invoked after `promise_setup`';
+ tests.complete();
+ }
+ var test_name = get_test_name(func, name);
+ var test_obj = new Test(test_name, properties);
+ var value = test_obj.step(func, test_obj, test_obj);
+
+ if (value !== undefined) {
+ var msg = 'Test named "' + test_name +
+ '" passed a function to `test` that returned a value.';
+
+ try {
+ if (value && typeof value.then === 'function') {
+ msg += ' Consider using `promise_test` instead when ' +
+ 'using Promises or async/await.';
+ }
+ } catch (err) {}
+
+ tests.status.status = tests.status.ERROR;
+ tests.status.message = msg;
+ }
+
+ if (test_obj.phase === test_obj.phases.STARTED) {
+ test_obj.done();
+ }
+ }
+
+ /**
+ * Create an asynchronous test
+ *
+ * @param {TestFunction|string} funcOrName - Initial step function
+ * to call immediately with the test name as an argument (if any),
+ * or name of the test.
+ * @param {String} name - Test name (if a test function was
+ * provided). This must be unique in a given file and must be
+ * invariant between runs.
+ * @returns {Test} An object representing the ongoing test.
+ */
+ function async_test(func, name, properties)
+ {
+ if (tests.promise_setup_called) {
+ tests.status.status = tests.status.ERROR;
+ tests.status.message = '`async_test` invoked after `promise_setup`';
+ tests.complete();
+ }
+ if (typeof func !== "function") {
+ properties = name;
+ name = func;
+ func = null;
+ }
+ var test_name = get_test_name(func, name);
+ var test_obj = new Test(test_name, properties);
+ if (func) {
+ var value = test_obj.step(func, test_obj, test_obj);
+
+ // Test authors sometimes return values to async_test, expecting us
+ // to handle the value somehow. Make doing so a harness error to be
+ // clear this is invalid, and point authors to promise_test if it
+ // may be appropriate.
+ //
+ // Note that we only perform this check on the initial function
+ // passed to async_test, not on any later steps - we haven't seen a
+ // consistent problem with those (and it's harder to check).
+ if (value !== undefined) {
+ var msg = 'Test named "' + test_name +
+ '" passed a function to `async_test` that returned a value.';
+
+ try {
+ if (value && typeof value.then === 'function') {
+ msg += ' Consider using `promise_test` instead when ' +
+ 'using Promises or async/await.';
+ }
+ } catch (err) {}
+
+ tests.set_status(tests.status.ERROR, msg);
+ tests.complete();
+ }
+ }
+ return test_obj;
+ }
+
+ /**
+ * Create a promise test.
+ *
+ * Promise tests are tests which are represented by a promise
+ * object. If the promise is fulfilled the test passes, if it's
+ * rejected the test fails, otherwise the test passes.
+ *
+ * @param {TestFunction} func - Test function. This must return a
+ * promise. The test is automatically marked as complete once the
+ * promise settles.
+ * @param {String} name - Test name. This must be unique in a
+ * given file and must be invariant between runs.
+ */
+ function promise_test(func, name, properties) {
+ if (typeof func !== "function") {
+ properties = name;
+ name = func;
+ func = null;
+ }
+ var test_name = get_test_name(func, name);
+ var test = new Test(test_name, properties);
+ test._is_promise_test = true;
+
+ // If there is no promise tests queue make one.
+ if (!tests.promise_tests) {
+ tests.promise_tests = Promise.resolve();
+ }
+ tests.promise_tests = tests.promise_tests.then(function() {
+ return new Promise(function(resolve) {
+ var promise = test.step(func, test, test);
+
+ test.step(function() {
+ assert(!!promise, "promise_test", null,
+ "test body must return a 'thenable' object (received ${value})",
+ {value:promise});
+ assert(typeof promise.then === "function", "promise_test", null,
+ "test body must return a 'thenable' object (received an object with no `then` method)",
+ null);
+ });
+
+ // Test authors may use the `step` method within a
+ // `promise_test` even though this reflects a mixture of
+ // asynchronous control flow paradigms. The "done" callback
+ // should be registered prior to the resolution of the
+ // user-provided Promise to avoid timeouts in cases where the
+ // Promise does not settle but a `step` function has thrown an
+ // error.
+ add_test_done_callback(test, resolve);
+
+ Promise.resolve(promise)
+ .catch(test.step_func(
+ function(value) {
+ if (value instanceof AssertionError) {
+ throw value;
+ }
+ assert(false, "promise_test", null,
+ "Unhandled rejection with value: ${value}", {value:value});
+ }))
+ .then(function() {
+ test.done();
+ });
+ });
+ });
+ }
+
+ /**
+ * Make a copy of a Promise in the current realm.
+ *
+ * @param {Promise} promise the given promise that may be from a different
+ * realm
+ * @returns {Promise}
+ *
+ * An arbitrary promise provided by the caller may have originated
+ * in another frame that have since navigated away, rendering the
+ * frame's document inactive. Such a promise cannot be used with
+ * `await` or Promise.resolve(), as microtasks associated with it
+ * may be prevented from being run. See `issue
+ * 5319<https://github.com/whatwg/html/issues/5319>`_ for a
+ * particular case.
+ *
+ * In functions we define here, there is an expectation from the caller
+ * that the promise is from the current realm, that can always be used with
+ * `await`, etc. We therefore create a new promise in this realm that
+ * inherit the value and status from the given promise.
+ */
+
+ function bring_promise_to_current_realm(promise) {
+ return new Promise(promise.then.bind(promise));
+ }
+
+ /**
+ * Assert that a Promise is rejected with the right ECMAScript exception.
+ *
+ * @param {Test} test - the `Test` to use for the assertion.
+ * @param {Function} constructor - The expected exception constructor.
+ * @param {Promise} promise - The promise that's expected to
+ * reject with the given exception.
+ * @param {string} [description] Error message to add to assert in case of
+ * failure.
+ */
+ function promise_rejects_js(test, constructor, promise, description) {
+ return bring_promise_to_current_realm(promise)
+ .then(test.unreached_func("Should have rejected: " + description))
+ .catch(function(e) {
+ assert_throws_js_impl(constructor, function() { throw e },
+ description, "promise_rejects_js");
+ });
+ }
+
+ /**
+ * Assert that a Promise is rejected with the right DOMException.
+ *
+ * For the remaining arguments, there are two ways of calling
+ * promise_rejects_dom:
+ *
+ * 1) If the DOMException is expected to come from the current global, the
+ * third argument should be the promise expected to reject, and a fourth,
+ * optional, argument is the assertion description.
+ *
+ * 2) If the DOMException is expected to come from some other global, the
+ * third argument should be the DOMException constructor from that global,
+ * the fourth argument the promise expected to reject, and the fifth,
+ * optional, argument the assertion description.
+ *
+ * @param {Test} test - the `Test` to use for the assertion.
+ * @param {number|string} type - See documentation for
+ * `assert_throws_dom <#assert_throws_dom>`_.
+ * @param {Function} promiseOrConstructor - Either the constructor
+ * for the expected exception (if the exception comes from another
+ * global), or the promise that's expected to reject (if the
+ * exception comes from the current global).
+ * @param {Function|string} descriptionOrPromise - Either the
+ * promise that's expected to reject (if the exception comes from
+ * another global), or the optional description of the condition
+ * being tested (if the exception comes from the current global).
+ * @param {string} [description] - Description of the condition
+ * being tested (if the exception comes from another global).
+ *
+ */
+ function promise_rejects_dom(test, type, promiseOrConstructor, descriptionOrPromise, maybeDescription) {
+ let constructor, promise, description;
+ if (typeof promiseOrConstructor === "function" &&
+ promiseOrConstructor.name === "DOMException") {
+ constructor = promiseOrConstructor;
+ promise = descriptionOrPromise;
+ description = maybeDescription;
+ } else {
+ constructor = self.DOMException;
+ promise = promiseOrConstructor;
+ description = descriptionOrPromise;
+ assert(maybeDescription === undefined,
+ "Too many args pased to no-constructor version of promise_rejects_dom");
+ }
+ return bring_promise_to_current_realm(promise)
+ .then(test.unreached_func("Should have rejected: " + description))
+ .catch(function(e) {
+ assert_throws_dom_impl(type, function() { throw e }, description,
+ "promise_rejects_dom", constructor);
+ });
+ }
+
+ /**
+ * Assert that a Promise is rejected with the provided value.
+ *
+ * @param {Test} test - the `Test` to use for the assertion.
+ * @param {Any} exception - The expected value of the rejected promise.
+ * @param {Promise} promise - The promise that's expected to
+ * reject.
+ * @param {string} [description] Error message to add to assert in case of
+ * failure.
+ */
+ function promise_rejects_exactly(test, exception, promise, description) {
+ return bring_promise_to_current_realm(promise)
+ .then(test.unreached_func("Should have rejected: " + description))
+ .catch(function(e) {
+ assert_throws_exactly_impl(exception, function() { throw e },
+ description, "promise_rejects_exactly");
+ });
+ }
+
+ /**
+ * Allow DOM events to be handled using Promises.
+ *
+ * This can make it a lot easier to test a very specific series of events,
+ * including ensuring that unexpected events are not fired at any point.
+ *
+ * `EventWatcher` will assert if an event occurs while there is no `wait_for`
+ * created Promise waiting to be fulfilled, or if the event is of a different type
+ * to the type currently expected. This ensures that only the events that are
+ * expected occur, in the correct order, and with the correct timing.
+ *
+ * @constructor
+ * @param {Test} test - The `Test` to use for the assertion.
+ * @param {EventTarget} watchedNode - The target expected to receive the events.
+ * @param {string[]} eventTypes - List of events to watch for.
+ * @param {Promise} timeoutPromise - Promise that will cause the
+ * test to be set to `TIMEOUT` once fulfilled.
+ *
+ */
+ function EventWatcher(test, watchedNode, eventTypes, timeoutPromise)
+ {
+ if (typeof eventTypes == 'string') {
+ eventTypes = [eventTypes];
+ }
+
+ var waitingFor = null;
+
+ // This is null unless we are recording all events, in which case it
+ // will be an Array object.
+ var recordedEvents = null;
+
+ var eventHandler = test.step_func(function(evt) {
+ assert_true(!!waitingFor,
+ 'Not expecting event, but got ' + evt.type + ' event');
+ assert_equals(evt.type, waitingFor.types[0],
+ 'Expected ' + waitingFor.types[0] + ' event, but got ' +
+ evt.type + ' event instead');
+
+ if (Array.isArray(recordedEvents)) {
+ recordedEvents.push(evt);
+ }
+
+ if (waitingFor.types.length > 1) {
+ // Pop first event from array
+ waitingFor.types.shift();
+ return;
+ }
+ // We need to null out waitingFor before calling the resolve function
+ // since the Promise's resolve handlers may call wait_for() which will
+ // need to set waitingFor.
+ var resolveFunc = waitingFor.resolve;
+ waitingFor = null;
+ // Likewise, we should reset the state of recordedEvents.
+ var result = recordedEvents || evt;
+ recordedEvents = null;
+ resolveFunc(result);
+ });
+
+ for (var i = 0; i < eventTypes.length; i++) {
+ watchedNode.addEventListener(eventTypes[i], eventHandler, false);
+ }
+
+ /**
+ * Returns a Promise that will resolve after the specified event or
+ * series of events has occurred.
+ *
+ * @param {Object} options An optional options object. If the 'record' property
+ * on this object has the value 'all', when the Promise
+ * returned by this function is resolved, *all* Event
+ * objects that were waited for will be returned as an
+ * array.
+ *
+ * @example
+ * const watcher = new EventWatcher(t, div, [ 'animationstart',
+ * 'animationiteration',
+ * 'animationend' ]);
+ * return watcher.wait_for([ 'animationstart', 'animationend' ],
+ * { record: 'all' }).then(evts => {
+ * assert_equals(evts[0].elapsedTime, 0.0);
+ * assert_equals(evts[1].elapsedTime, 2.0);
+ * });
+ */
+ this.wait_for = function(types, options) {
+ if (waitingFor) {
+ return Promise.reject('Already waiting for an event or events');
+ }
+ if (typeof types == 'string') {
+ types = [types];
+ }
+ if (options && options.record && options.record === 'all') {
+ recordedEvents = [];
+ }
+ return new Promise(function(resolve, reject) {
+ var timeout = test.step_func(function() {
+ // If the timeout fires after the events have been received
+ // or during a subsequent call to wait_for, ignore it.
+ if (!waitingFor || waitingFor.resolve !== resolve)
+ return;
+
+ // This should always fail, otherwise we should have
+ // resolved the promise.
+ assert_true(waitingFor.types.length == 0,
+ 'Timed out waiting for ' + waitingFor.types.join(', '));
+ var result = recordedEvents;
+ recordedEvents = null;
+ var resolveFunc = waitingFor.resolve;
+ waitingFor = null;
+ resolveFunc(result);
+ });
+
+ if (timeoutPromise) {
+ timeoutPromise().then(timeout);
+ }
+
+ waitingFor = {
+ types: types,
+ resolve: resolve,
+ reject: reject
+ };
+ });
+ };
+
+ /**
+ * Stop listening for events
+ */
+ function stop_watching() {
+ for (var i = 0; i < eventTypes.length; i++) {
+ watchedNode.removeEventListener(eventTypes[i], eventHandler, false);
+ }
+ };
+
+ test._add_cleanup(stop_watching);
+
+ return this;
+ }
+ expose(EventWatcher, 'EventWatcher');
+
+ /**
+ * @typedef {Object} SettingsObject
+ * @property {bool} single_test - Use the single-page-test
+ * mode. In this mode the Document represents a single
+ * `async_test`. Asserts may be used directly without requiring
+ * `Test.step` or similar wrappers, and any exceptions set the
+ * status of the test rather than the status of the harness.
+ * @property {bool} allow_uncaught_exception - don't treat an
+ * uncaught exception as an error; needed when e.g. testing the
+ * `window.onerror` handler.
+ * @property {boolean} explicit_done - Wait for a call to `done()`
+ * before declaring all tests complete (this is always true for
+ * single-page tests).
+ * @property hide_test_state - hide the test state output while
+ * the test is running; This is helpful when the output of the test state
+ * may interfere the test results.
+ * @property {bool} explicit_timeout - disable file timeout; only
+ * stop waiting for results when the `timeout()` function is
+ * called This should typically only be set for manual tests, or
+ * by a test runner that providees its own timeout mechanism.
+ * @property {number} timeout_multiplier - Multiplier to apply to
+ * per-test timeouts. This should only be set by a test runner.
+ * @property {Document} output_document - The document to which
+ * results should be logged. By default this is the current
+ * document but could be an ancestor document in some cases e.g. a
+ * SVG test loaded in an HTML wrapper
+ *
+ */
+
+ /**
+ * Configure the harness
+ *
+ * @param {Function|SettingsObject} funcOrProperties - Either a
+ * setup function to run, or a set of properties. If this is a
+ * function that function is run synchronously. Any exception in
+ * the function will set the overall harness status to `ERROR`.
+ * @param {SettingsObject} maybeProperties - An object containing
+ * the settings to use, if the first argument is a function.
+ *
+ */
+ function setup(func_or_properties, maybe_properties)
+ {
+ var func = null;
+ var properties = {};
+ if (arguments.length === 2) {
+ func = func_or_properties;
+ properties = maybe_properties;
+ } else if (func_or_properties instanceof Function) {
+ func = func_or_properties;
+ } else {
+ properties = func_or_properties;
+ }
+ tests.setup(func, properties);
+ test_environment.on_new_harness_properties(properties);
+ }
+
+ /**
+ * Configure the harness, waiting for a promise to resolve
+ * before running any `promise_test` tests.
+ *
+ * @param {Function} func - Function returning a promise that's
+ * run synchronously. Promise tests are not run until after this
+ * function has resolved.
+ * @param {SettingsObject} [properties] - An object containing
+ * the harness settings to use.
+ *
+ */
+ function promise_setup(func, properties={})
+ {
+ if (typeof func !== "function") {
+ tests.set_status(tests.status.ERROR,
+ "promise_test invoked without a function");
+ tests.complete();
+ return;
+ }
+ tests.promise_setup_called = true;
+
+ if (!tests.promise_tests) {
+ tests.promise_tests = Promise.resolve();
+ }
+
+ tests.promise_tests = tests.promise_tests
+ .then(function()
+ {
+ var result;
+
+ tests.setup(null, properties);
+ result = func();
+ test_environment.on_new_harness_properties(properties);
+
+ if (!result || typeof result.then !== "function") {
+ throw "Non-thenable returned by function passed to `promise_setup`";
+ }
+ return result;
+ })
+ .catch(function(e)
+ {
+ tests.set_status(tests.status.ERROR,
+ String(e),
+ e && e.stack);
+ tests.complete();
+ });
+ }
+
+ /**
+ * Mark test loading as complete.
+ *
+ * Typically this function is called implicitly on page load; it's
+ * only necessary for users to call this when either the
+ * ``explicit_done`` or ``single_page`` properties have been set
+ * via the :js:func:`setup` function.
+ *
+ * For single page tests this marks the test as complete and sets its status.
+ * For other tests, this marks test loading as complete, but doesn't affect ongoing tests.
+ */
+ function done() {
+ if (tests.tests.length === 0) {
+ // `done` is invoked after handling uncaught exceptions, so if the
+ // harness status is already set, the corresponding message is more
+ // descriptive than the generic message defined here.
+ if (tests.status.status === null) {
+ tests.status.status = tests.status.ERROR;
+ tests.status.message = "done() was called without first defining any tests";
+ }
+
+ tests.complete();
+ return;
+ }
+ if (tests.file_is_test) {
+ // file is test files never have asynchronous cleanup logic,
+ // meaning the fully-synchronous `done` function can be used here.
+ tests.tests[0].done();
+ }
+ tests.end_wait();
+ }
+
+ /**
+ * @deprecated generate a list of tests from a function and list of arguments
+ *
+ * This is deprecated because it runs all the tests outside of the test functions
+ * and as a result any test throwing an exception will result in no tests being
+ * run. In almost all cases, you should simply call test within the loop you would
+ * use to generate the parameter list array.
+ *
+ * @param {Function} func - The function that will be called for each generated tests.
+ * @param {Any[][]} args - An array of arrays. Each nested array
+ * has the structure `[testName, ...testArgs]`. For each of these nested arrays
+ * array, a test is generated with name `testName` and test function equivalent to
+ * `func(..testArgs)`.
+ */
+ function generate_tests(func, args, properties) {
+ forEach(args, function(x, i)
+ {
+ var name = x[0];
+ test(function()
+ {
+ func.apply(this, x.slice(1));
+ },
+ name,
+ Array.isArray(properties) ? properties[i] : properties);
+ });
+ }
+
+ /**
+ * @deprecated
+ *
+ * Register a function as a DOM event listener to the
+ * given object for the event bubbling phase.
+ *
+ * @param {EventTarget} object - Event target
+ * @param {string} event - Event name
+ * @param {Function} callback - Event handler.
+ */
+ function on_event(object, event, callback)
+ {
+ object.addEventListener(event, callback, false);
+ }
+
+ /**
+ * Global version of :js:func:`Test.step_timeout` for use in single page tests.
+ *
+ * @param {Function} func - Function to run after the timeout
+ * @param {number} timeout - Time in ms to wait before running the
+ * test step. The actual wait time is ``timeout`` x
+ * ``timeout_multiplier``.
+ */
+ function step_timeout(func, timeout) {
+ var outer_this = this;
+ var args = Array.prototype.slice.call(arguments, 2);
+ return setTimeout(function() {
+ func.apply(outer_this, args);
+ }, timeout * tests.timeout_multiplier);
+ }
+
+ expose(test, 'test');
+ expose(async_test, 'async_test');
+ expose(promise_test, 'promise_test');
+ expose(promise_rejects_js, 'promise_rejects_js');
+ expose(promise_rejects_dom, 'promise_rejects_dom');
+ expose(promise_rejects_exactly, 'promise_rejects_exactly');
+ expose(generate_tests, 'generate_tests');
+ expose(setup, 'setup');
+ expose(promise_setup, 'promise_setup');
+ expose(done, 'done');
+ expose(on_event, 'on_event');
+ expose(step_timeout, 'step_timeout');
+
+ /*
+ * Return a string truncated to the given length, with ... added at the end
+ * if it was longer.
+ */
+ function truncate(s, len)
+ {
+ if (s.length > len) {
+ return s.substring(0, len - 3) + "...";
+ }
+ return s;
+ }
+
+ /*
+ * Return true if object is probably a Node object.
+ */
+ function is_node(object)
+ {
+ // I use duck-typing instead of instanceof, because
+ // instanceof doesn't work if the node is from another window (like an
+ // iframe's contentWindow):
+ // http://www.w3.org/Bugs/Public/show_bug.cgi?id=12295
+ try {
+ var has_node_properties = ("nodeType" in object &&
+ "nodeName" in object &&
+ "nodeValue" in object &&
+ "childNodes" in object);
+ } catch (e) {
+ // We're probably cross-origin, which means we aren't a node
+ return false;
+ }
+
+ if (has_node_properties) {
+ try {
+ object.nodeType;
+ } catch (e) {
+ // The object is probably Node.prototype or another prototype
+ // object that inherits from it, and not a Node instance.
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ var replacements = {
+ "0": "0",
+ "1": "x01",
+ "2": "x02",
+ "3": "x03",
+ "4": "x04",
+ "5": "x05",
+ "6": "x06",
+ "7": "x07",
+ "8": "b",
+ "9": "t",
+ "10": "n",
+ "11": "v",
+ "12": "f",
+ "13": "r",
+ "14": "x0e",
+ "15": "x0f",
+ "16": "x10",
+ "17": "x11",
+ "18": "x12",
+ "19": "x13",
+ "20": "x14",
+ "21": "x15",
+ "22": "x16",
+ "23": "x17",
+ "24": "x18",
+ "25": "x19",
+ "26": "x1a",
+ "27": "x1b",
+ "28": "x1c",
+ "29": "x1d",
+ "30": "x1e",
+ "31": "x1f",
+ "0xfffd": "ufffd",
+ "0xfffe": "ufffe",
+ "0xffff": "uffff",
+ };
+
+ /**
+ * Convert a value to a nice, human-readable string
+ *
+ * When many JavaScript Object values are coerced to a String, the
+ * resulting value will be ``"[object Object]"``. This obscures
+ * helpful information, making the coerced value unsuitable for
+ * use in assertion messages, test names, and debugging
+ * statements. `format_value` produces more distinctive string
+ * representations of many kinds of objects, including arrays and
+ * the more important DOM Node types. It also translates String
+ * values containing control characters to include human-readable
+ * representations.
+ *
+ * @example
+ * // "Document node with 2 children"
+ * format_value(document);
+ * @example
+ * // "\"foo\\uffffbar\""
+ * format_value("foo\uffffbar");
+ * @example
+ * // "[-0, Infinity]"
+ * format_value([-0, Infinity]);
+ * @param {Any} val - The value to convert to a string.
+ * @returns {string} - A string representation of ``val``, optimised for human readability.
+ */
+ function format_value(val, seen)
+ {
+ if (!seen) {
+ seen = [];
+ }
+ if (typeof val === "object" && val !== null) {
+ if (seen.indexOf(val) >= 0) {
+ return "[...]";
+ }
+ seen.push(val);
+ }
+ if (Array.isArray(val)) {
+ let output = "[";
+ if (val.beginEllipsis !== undefined) {
+ output += "…, ";
+ }
+ output += val.map(function(x) {return format_value(x, seen);}).join(", ");
+ if (val.endEllipsis !== undefined) {
+ output += ", …";
+ }
+ return output + "]";
+ }
+
+ switch (typeof val) {
+ case "string":
+ val = val.replace(/\\/g, "\\\\");
+ for (var p in replacements) {
+ var replace = "\\" + replacements[p];
+ val = val.replace(RegExp(String.fromCharCode(p), "g"), replace);
+ }
+ return '"' + val.replace(/"/g, '\\"') + '"';
+ case "boolean":
+ case "undefined":
+ return String(val);
+ case "number":
+ // In JavaScript, -0 === 0 and String(-0) == "0", so we have to
+ // special-case.
+ if (val === -0 && 1/val === -Infinity) {
+ return "-0";
+ }
+ return String(val);
+ case "object":
+ if (val === null) {
+ return "null";
+ }
+
+ // Special-case Node objects, since those come up a lot in my tests. I
+ // ignore namespaces.
+ if (is_node(val)) {
+ switch (val.nodeType) {
+ case Node.ELEMENT_NODE:
+ var ret = "<" + val.localName;
+ for (var i = 0; i < val.attributes.length; i++) {
+ ret += " " + val.attributes[i].name + '="' + val.attributes[i].value + '"';
+ }
+ ret += ">" + val.innerHTML + "</" + val.localName + ">";
+ return "Element node " + truncate(ret, 60);
+ case Node.TEXT_NODE:
+ return 'Text node "' + truncate(val.data, 60) + '"';
+ case Node.PROCESSING_INSTRUCTION_NODE:
+ return "ProcessingInstruction node with target " + format_value(truncate(val.target, 60)) + " and data " + format_value(truncate(val.data, 60));
+ case Node.COMMENT_NODE:
+ return "Comment node <!--" + truncate(val.data, 60) + "-->";
+ case Node.DOCUMENT_NODE:
+ return "Document node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children");
+ case Node.DOCUMENT_TYPE_NODE:
+ return "DocumentType node";
+ case Node.DOCUMENT_FRAGMENT_NODE:
+ return "DocumentFragment node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children");
+ default:
+ return "Node object of unknown type";
+ }
+ }
+
+ /* falls through */
+ default:
+ try {
+ return typeof val + ' "' + truncate(String(val), 1000) + '"';
+ } catch(e) {
+ return ("[stringifying object threw " + String(e) +
+ " with type " + String(typeof e) + "]");
+ }
+ }
+ }
+ expose(format_value, "format_value");
+
+ /*
+ * Assertions
+ */
+
+ function expose_assert(f, name) {
+ function assert_wrapper(...args) {
+ let status = Test.statuses.TIMEOUT;
+ let stack = null;
+ let new_assert_index = null;
+ try {
+ if (settings.debug) {
+ console.debug("ASSERT", name, tests.current_test && tests.current_test.name, args);
+ }
+ if (tests.output) {
+ tests.set_assert(name, args);
+ // Remember the newly pushed assert's index, because `apply`
+ // below might push new asserts.
+ new_assert_index = tests.asserts_run.length - 1;
+ }
+ const rv = f.apply(undefined, args);
+ status = Test.statuses.PASS;
+ return rv;
+ } catch(e) {
+ status = Test.statuses.FAIL;
+ stack = e.stack ? e.stack : null;
+ throw e;
+ } finally {
+ if (tests.output && !stack) {
+ stack = get_stack();
+ }
+ if (tests.output) {
+ tests.set_assert_status(new_assert_index, status, stack);
+ }
+ }
+ }
+ expose(assert_wrapper, name);
+ }
+
+ /**
+ * Assert that ``actual`` is strictly true
+ *
+ * @param {Any} actual - Value that is asserted to be true
+ * @param {string} [description] - Description of the condition being tested
+ */
+ function assert_true(actual, description)
+ {
+ assert(actual === true, "assert_true", description,
+ "expected true got ${actual}", {actual:actual});
+ }
+ expose_assert(assert_true, "assert_true");
+
+ /**
+ * Assert that ``actual`` is strictly false
+ *
+ * @param {Any} actual - Value that is asserted to be false
+ * @param {string} [description] - Description of the condition being tested
+ */
+ function assert_false(actual, description)
+ {
+ assert(actual === false, "assert_false", description,
+ "expected false got ${actual}", {actual:actual});
+ }
+ expose_assert(assert_false, "assert_false");
+
+ function same_value(x, y) {
+ if (y !== y) {
+ //NaN case
+ return x !== x;
+ }
+ if (x === 0 && y === 0) {
+ //Distinguish +0 and -0
+ return 1/x === 1/y;
+ }
+ return x === y;
+ }
+
+ /**
+ * Assert that ``actual`` is the same value as ``expected``.
+ *
+ * For objects this compares by cobject identity; for primitives
+ * this distinguishes between 0 and -0, and has correct handling
+ * of NaN.
+ *
+ * @param {Any} actual - Test value.
+ * @param {Any} expected - Expected value.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_equals(actual, expected, description)
+ {
+ /*
+ * Test if two primitives are equal or two objects
+ * are the same object
+ */
+ if (typeof actual != typeof expected) {
+ assert(false, "assert_equals", description,
+ "expected (" + typeof expected + ") ${expected} but got (" + typeof actual + ") ${actual}",
+ {expected:expected, actual:actual});
+ return;
+ }
+ assert(same_value(actual, expected), "assert_equals", description,
+ "expected ${expected} but got ${actual}",
+ {expected:expected, actual:actual});
+ }
+ expose_assert(assert_equals, "assert_equals");
+
+ /**
+ * Assert that ``actual`` is not the same value as ``expected``.
+ *
+ * Comparison is as for :js:func:`assert_equals`.
+ *
+ * @param {Any} actual - Test value.
+ * @param {Any} expected - The value ``actual`` is expected to be different to.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_not_equals(actual, expected, description)
+ {
+ assert(!same_value(actual, expected), "assert_not_equals", description,
+ "got disallowed value ${actual}",
+ {actual:actual});
+ }
+ expose_assert(assert_not_equals, "assert_not_equals");
+
+ /**
+ * Assert that ``expected`` is an array and ``actual`` is one of the members.
+ * This is implemented using ``indexOf``, so doesn't handle NaN or ±0 correctly.
+ *
+ * @param {Any} actual - Test value.
+ * @param {Array} expected - An array that ``actual`` is expected to
+ * be a member of.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_in_array(actual, expected, description)
+ {
+ assert(expected.indexOf(actual) != -1, "assert_in_array", description,
+ "value ${actual} not in array ${expected}",
+ {actual:actual, expected:expected});
+ }
+ expose_assert(assert_in_array, "assert_in_array");
+
+ // This function was deprecated in July of 2015.
+ // See https://github.com/web-platform-tests/wpt/issues/2033
+ /**
+ * @deprecated
+ * Recursively compare two objects for equality.
+ *
+ * See `Issue 2033
+ * <https://github.com/web-platform-tests/wpt/issues/2033>`_ for
+ * more information.
+ *
+ * @param {Object} actual - Test value.
+ * @param {Object} expected - Expected value.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_object_equals(actual, expected, description)
+ {
+ assert(typeof actual === "object" && actual !== null, "assert_object_equals", description,
+ "value is ${actual}, expected object",
+ {actual: actual});
+ //This needs to be improved a great deal
+ function check_equal(actual, expected, stack)
+ {
+ stack.push(actual);
+
+ var p;
+ for (p in actual) {
+ assert(expected.hasOwnProperty(p), "assert_object_equals", description,
+ "unexpected property ${p}", {p:p});
+
+ if (typeof actual[p] === "object" && actual[p] !== null) {
+ if (stack.indexOf(actual[p]) === -1) {
+ check_equal(actual[p], expected[p], stack);
+ }
+ } else {
+ assert(same_value(actual[p], expected[p]), "assert_object_equals", description,
+ "property ${p} expected ${expected} got ${actual}",
+ {p:p, expected:expected[p], actual:actual[p]});
+ }
+ }
+ for (p in expected) {
+ assert(actual.hasOwnProperty(p),
+ "assert_object_equals", description,
+ "expected property ${p} missing", {p:p});
+ }
+ stack.pop();
+ }
+ check_equal(actual, expected, []);
+ }
+ expose_assert(assert_object_equals, "assert_object_equals");
+
+ /**
+ * Assert that ``actual`` and ``expected`` are both arrays, and that the array properties of
+ * ``actual`` and ``expected`` are all the same value (as for :js:func:`assert_equals`).
+ *
+ * @param {Array} actual - Test array.
+ * @param {Array} expected - Array that is expected to contain the same values as ``actual``.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_array_equals(actual, expected, description)
+ {
+ const max_array_length = 20;
+ function shorten_array(arr, offset = 0) {
+ // Make ", …" only show up when it would likely reduce the length, not accounting for
+ // fonts.
+ if (arr.length < max_array_length + 2) {
+ return arr;
+ }
+ // By default we want half the elements after the offset and half before
+ // But if that takes us past the end of the array, we have more before, and
+ // if it takes us before the start we have more after.
+ const length_after_offset = Math.floor(max_array_length / 2);
+ let upper_bound = Math.min(length_after_offset + offset, arr.length);
+ const lower_bound = Math.max(upper_bound - max_array_length, 0);
+
+ if (lower_bound === 0) {
+ upper_bound = max_array_length;
+ }
+
+ const output = arr.slice(lower_bound, upper_bound);
+ if (lower_bound > 0) {
+ output.beginEllipsis = true;
+ }
+ if (upper_bound < arr.length) {
+ output.endEllipsis = true;
+ }
+ return output;
+ }
+
+ assert(typeof actual === "object" && actual !== null && "length" in actual,
+ "assert_array_equals", description,
+ "value is ${actual}, expected array",
+ {actual:actual});
+ assert(actual.length === expected.length,
+ "assert_array_equals", description,
+ "lengths differ, expected array ${expected} length ${expectedLength}, got ${actual} length ${actualLength}",
+ {expected:shorten_array(expected, expected.length - 1), expectedLength:expected.length,
+ actual:shorten_array(actual, actual.length - 1), actualLength:actual.length
+ });
+
+ for (var i = 0; i < actual.length; i++) {
+ assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i),
+ "assert_array_equals", description,
+ "expected property ${i} to be ${expected} but was ${actual} (expected array ${arrayExpected} got ${arrayActual})",
+ {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing",
+ actual:actual.hasOwnProperty(i) ? "present" : "missing",
+ arrayExpected:shorten_array(expected, i), arrayActual:shorten_array(actual, i)});
+ assert(same_value(expected[i], actual[i]),
+ "assert_array_equals", description,
+ "expected property ${i} to be ${expected} but got ${actual} (expected array ${arrayExpected} got ${arrayActual})",
+ {i:i, expected:expected[i], actual:actual[i],
+ arrayExpected:shorten_array(expected, i), arrayActual:shorten_array(actual, i)});
+ }
+ }
+ expose_assert(assert_array_equals, "assert_array_equals");
+
+ /**
+ * Assert that each array property in ``actual`` is a number within
+ * ± `epsilon` of the corresponding property in `expected`.
+ *
+ * @param {Array} actual - Array of test values.
+ * @param {Array} expected - Array of values expected to be close to the values in ``actual``.
+ * @param {number} epsilon - Magnitude of allowed difference
+ * between each value in ``actual`` and ``expected``.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_array_approx_equals(actual, expected, epsilon, description)
+ {
+ /*
+ * Test if two primitive arrays are equal within +/- epsilon
+ */
+ assert(actual.length === expected.length,
+ "assert_array_approx_equals", description,
+ "lengths differ, expected ${expected} got ${actual}",
+ {expected:expected.length, actual:actual.length});
+
+ for (var i = 0; i < actual.length; i++) {
+ assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i),
+ "assert_array_approx_equals", description,
+ "property ${i}, property expected to be ${expected} but was ${actual}",
+ {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing",
+ actual:actual.hasOwnProperty(i) ? "present" : "missing"});
+ assert(typeof actual[i] === "number",
+ "assert_array_approx_equals", description,
+ "property ${i}, expected a number but got a ${type_actual}",
+ {i:i, type_actual:typeof actual[i]});
+ assert(Math.abs(actual[i] - expected[i]) <= epsilon,
+ "assert_array_approx_equals", description,
+ "property ${i}, expected ${expected} +/- ${epsilon}, expected ${expected} but got ${actual}",
+ {i:i, expected:expected[i], actual:actual[i], epsilon:epsilon});
+ }
+ }
+ expose_assert(assert_array_approx_equals, "assert_array_approx_equals");
+
+ /**
+ * Assert that ``actual`` is within ± ``epsilon`` of ``expected``.
+ *
+ * @param {number} actual - Test value.
+ * @param {number} expected - Value number is expected to be close to.
+ * @param {number} epsilon - Magnitude of allowed difference between ``actual`` and ``expected``.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_approx_equals(actual, expected, epsilon, description)
+ {
+ /*
+ * Test if two primitive numbers are equal within +/- epsilon
+ */
+ assert(typeof actual === "number",
+ "assert_approx_equals", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ // The epsilon math below does not place nice with NaN and Infinity
+ // But in this case Infinity = Infinity and NaN = NaN
+ if (isFinite(actual) || isFinite(expected)) {
+ assert(Math.abs(actual - expected) <= epsilon,
+ "assert_approx_equals", description,
+ "expected ${expected} +/- ${epsilon} but got ${actual}",
+ {expected:expected, actual:actual, epsilon:epsilon});
+ } else {
+ assert_equals(actual, expected);
+ }
+ }
+ expose_assert(assert_approx_equals, "assert_approx_equals");
+
+ /**
+ * Assert that ``actual`` is a number less than ``expected``.
+ *
+ * @param {number} actual - Test value.
+ * @param {number} expected - Number that ``actual`` must be less than.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_less_than(actual, expected, description)
+ {
+ /*
+ * Test if a primitive number is less than another
+ */
+ assert(typeof actual === "number",
+ "assert_less_than", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ assert(actual < expected,
+ "assert_less_than", description,
+ "expected a number less than ${expected} but got ${actual}",
+ {expected:expected, actual:actual});
+ }
+ expose_assert(assert_less_than, "assert_less_than");
+
+ /**
+ * Assert that ``actual`` is a number greater than ``expected``.
+ *
+ * @param {number} actual - Test value.
+ * @param {number} expected - Number that ``actual`` must be greater than.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_greater_than(actual, expected, description)
+ {
+ /*
+ * Test if a primitive number is greater than another
+ */
+ assert(typeof actual === "number",
+ "assert_greater_than", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ assert(actual > expected,
+ "assert_greater_than", description,
+ "expected a number greater than ${expected} but got ${actual}",
+ {expected:expected, actual:actual});
+ }
+ expose_assert(assert_greater_than, "assert_greater_than");
+
+ /**
+ * Assert that ``actual`` is a number greater than ``lower`` and less
+ * than ``upper`` but not equal to either.
+ *
+ * @param {number} actual - Test value.
+ * @param {number} lower - Number that ``actual`` must be greater than.
+ * @param {number} upper - Number that ``actual`` must be less than.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_between_exclusive(actual, lower, upper, description)
+ {
+ /*
+ * Test if a primitive number is between two others
+ */
+ assert(typeof actual === "number",
+ "assert_between_exclusive", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ assert(actual > lower && actual < upper,
+ "assert_between_exclusive", description,
+ "expected a number greater than ${lower} " +
+ "and less than ${upper} but got ${actual}",
+ {lower:lower, upper:upper, actual:actual});
+ }
+ expose_assert(assert_between_exclusive, "assert_between_exclusive");
+
+ /**
+ * Assert that ``actual`` is a number less than or equal to ``expected``.
+ *
+ * @param {number} actual - Test value.
+ * @param {number} expected - Number that ``actual`` must be less
+ * than or equal to.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_less_than_equal(actual, expected, description)
+ {
+ /*
+ * Test if a primitive number is less than or equal to another
+ */
+ assert(typeof actual === "number",
+ "assert_less_than_equal", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ assert(actual <= expected,
+ "assert_less_than_equal", description,
+ "expected a number less than or equal to ${expected} but got ${actual}",
+ {expected:expected, actual:actual});
+ }
+ expose_assert(assert_less_than_equal, "assert_less_than_equal");
+
+ /**
+ * Assert that ``actual`` is a number greater than or equal to ``expected``.
+ *
+ * @param {number} actual - Test value.
+ * @param {number} expected - Number that ``actual`` must be greater
+ * than or equal to.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_greater_than_equal(actual, expected, description)
+ {
+ /*
+ * Test if a primitive number is greater than or equal to another
+ */
+ assert(typeof actual === "number",
+ "assert_greater_than_equal", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ assert(actual >= expected,
+ "assert_greater_than_equal", description,
+ "expected a number greater than or equal to ${expected} but got ${actual}",
+ {expected:expected, actual:actual});
+ }
+ expose_assert(assert_greater_than_equal, "assert_greater_than_equal");
+
+ /**
+ * Assert that ``actual`` is a number greater than or equal to ``lower`` and less
+ * than or equal to ``upper``.
+ *
+ * @param {number} actual - Test value.
+ * @param {number} lower - Number that ``actual`` must be greater than or equal to.
+ * @param {number} upper - Number that ``actual`` must be less than or equal to.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_between_inclusive(actual, lower, upper, description)
+ {
+ /*
+ * Test if a primitive number is between to two others or equal to either of them
+ */
+ assert(typeof actual === "number",
+ "assert_between_inclusive", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ assert(actual >= lower && actual <= upper,
+ "assert_between_inclusive", description,
+ "expected a number greater than or equal to ${lower} " +
+ "and less than or equal to ${upper} but got ${actual}",
+ {lower:lower, upper:upper, actual:actual});
+ }
+ expose_assert(assert_between_inclusive, "assert_between_inclusive");
+
+ /**
+ * Assert that ``actual`` matches the RegExp ``expected``.
+ *
+ * @param {String} actual - Test string.
+ * @param {RegExp} expected - RegExp ``actual`` must match.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_regexp_match(actual, expected, description) {
+ /*
+ * Test if a string (actual) matches a regexp (expected)
+ */
+ assert(expected.test(actual),
+ "assert_regexp_match", description,
+ "expected ${expected} but got ${actual}",
+ {expected:expected, actual:actual});
+ }
+ expose_assert(assert_regexp_match, "assert_regexp_match");
+
+ /**
+ * Assert that the class string of ``object`` as returned in
+ * ``Object.prototype.toString`` is equal to ``class_name``.
+ *
+ * @param {Object} object - Object to stringify.
+ * @param {string} class_string - Expected class string for ``object``.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_class_string(object, class_string, description) {
+ var actual = {}.toString.call(object);
+ var expected = "[object " + class_string + "]";
+ assert(same_value(actual, expected), "assert_class_string", description,
+ "expected ${expected} but got ${actual}",
+ {expected:expected, actual:actual});
+ }
+ expose_assert(assert_class_string, "assert_class_string");
+
+ /**
+ * Assert that ``object`` has an own property with name ``property_name``.
+ *
+ * @param {Object} object - Object that should have the given property.
+ * @param {string} property_name - Expected property name.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_own_property(object, property_name, description) {
+ assert(object.hasOwnProperty(property_name),
+ "assert_own_property", description,
+ "expected property ${p} missing", {p:property_name});
+ }
+ expose_assert(assert_own_property, "assert_own_property");
+
+ /**
+ * Assert that ``object`` does not have an own property with name ``property_name``.
+ *
+ * @param {Object} object - Object that should not have the given property.
+ * @param {string} property_name - Property name to test.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_not_own_property(object, property_name, description) {
+ assert(!object.hasOwnProperty(property_name),
+ "assert_not_own_property", description,
+ "unexpected property ${p} is found on object", {p:property_name});
+ }
+ expose_assert(assert_not_own_property, "assert_not_own_property");
+
+ function _assert_inherits(name) {
+ return function (object, property_name, description)
+ {
+ assert((typeof object === "object" && object !== null) ||
+ typeof object === "function" ||
+ // Or has [[IsHTMLDDA]] slot
+ String(object) === "[object HTMLAllCollection]",
+ name, description,
+ "provided value is not an object");
+
+ assert("hasOwnProperty" in object,
+ name, description,
+ "provided value is an object but has no hasOwnProperty method");
+
+ assert(!object.hasOwnProperty(property_name),
+ name, description,
+ "property ${p} found on object expected in prototype chain",
+ {p:property_name});
+
+ assert(property_name in object,
+ name, description,
+ "property ${p} not found in prototype chain",
+ {p:property_name});
+ };
+ }
+
+ /**
+ * Assert that ``object`` does not have an own property with name
+ * ``property_name``, but inherits one through the prototype chain.
+ *
+ * @param {Object} object - Object that should have the given property in its prototype chain.
+ * @param {string} property_name - Expected property name.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_inherits(object, property_name, description) {
+ return _assert_inherits("assert_inherits")(object, property_name, description);
+ }
+ expose_assert(assert_inherits, "assert_inherits");
+
+ /**
+ * Alias for :js:func:`insert_inherits`.
+ *
+ * @param {Object} object - Object that should have the given property in its prototype chain.
+ * @param {string} property_name - Expected property name.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_idl_attribute(object, property_name, description) {
+ return _assert_inherits("assert_idl_attribute")(object, property_name, description);
+ }
+ expose_assert(assert_idl_attribute, "assert_idl_attribute");
+
+
+ /**
+ * Assert that ``object`` has a property named ``property_name`` and that the property is readonly.
+ *
+ * Note: The implementation tries to update the named property, so
+ * any side effects of updating will be triggered. Users are
+ * encouraged to instead inspect the property descriptor of ``property_name`` on ``object``.
+ *
+ * @param {Object} object - Object that should have the given property in its prototype chain.
+ * @param {string} property_name - Expected property name.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_readonly(object, property_name, description)
+ {
+ var initial_value = object[property_name];
+ try {
+ //Note that this can have side effects in the case where
+ //the property has PutForwards
+ object[property_name] = initial_value + "a"; //XXX use some other value here?
+ assert(same_value(object[property_name], initial_value),
+ "assert_readonly", description,
+ "changing property ${p} succeeded",
+ {p:property_name});
+ } finally {
+ object[property_name] = initial_value;
+ }
+ }
+ expose_assert(assert_readonly, "assert_readonly");
+
+ /**
+ * Assert a JS Error with the expected constructor is thrown.
+ *
+ * @param {object} constructor The expected exception constructor.
+ * @param {Function} func Function which should throw.
+ * @param {string} [description] Error description for the case that the error is not thrown.
+ */
+ function assert_throws_js(constructor, func, description)
+ {
+ assert_throws_js_impl(constructor, func, description,
+ "assert_throws_js");
+ }
+ expose_assert(assert_throws_js, "assert_throws_js");
+
+ /**
+ * Like assert_throws_js but allows specifying the assertion type
+ * (assert_throws_js or promise_rejects_js, in practice).
+ */
+ function assert_throws_js_impl(constructor, func, description,
+ assertion_type)
+ {
+ try {
+ func.call(this);
+ assert(false, assertion_type, description,
+ "${func} did not throw", {func:func});
+ } catch (e) {
+ if (e instanceof AssertionError) {
+ throw e;
+ }
+
+ // Basic sanity-checks on the thrown exception.
+ assert(typeof e === "object",
+ assertion_type, description,
+ "${func} threw ${e} with type ${type}, not an object",
+ {func:func, e:e, type:typeof e});
+
+ assert(e !== null,
+ assertion_type, description,
+ "${func} threw null, not an object",
+ {func:func});
+
+ // Basic sanity-check on the passed-in constructor
+ assert(typeof constructor == "function",
+ assertion_type, description,
+ "${constructor} is not a constructor",
+ {constructor:constructor});
+ var obj = constructor;
+ while (obj) {
+ if (typeof obj === "function" &&
+ obj.name === "Error") {
+ break;
+ }
+ obj = Object.getPrototypeOf(obj);
+ }
+ assert(obj != null,
+ assertion_type, description,
+ "${constructor} is not an Error subtype",
+ {constructor:constructor});
+
+ // And checking that our exception is reasonable
+ assert(e.constructor === constructor &&
+ e.name === constructor.name,
+ assertion_type, description,
+ "${func} threw ${actual} (${actual_name}) expected instance of ${expected} (${expected_name})",
+ {func:func, actual:e, actual_name:e.name,
+ expected:constructor,
+ expected_name:constructor.name});
+ }
+ }
+
+ // TODO: Figure out how to document the overloads better.
+ // sphinx-js doesn't seem to handle @variation correctly,
+ // and only expects a single JSDoc entry per function.
+ /**
+ * Assert a DOMException with the expected type is thrown.
+ *
+ * There are two ways of calling assert_throws_dom:
+ *
+ * 1) If the DOMException is expected to come from the current global, the
+ * second argument should be the function expected to throw and a third,
+ * optional, argument is the assertion description.
+ *
+ * 2) If the DOMException is expected to come from some other global, the
+ * second argument should be the DOMException constructor from that global,
+ * the third argument the function expected to throw, and the fourth, optional,
+ * argument the assertion description.
+ *
+ * @param {number|string} type - The expected exception name or
+ * code. See the `table of names and codes
+ * <https://webidl.spec.whatwg.org/#dfn-error-names-table>`_. If a
+ * number is passed it should be one of the numeric code values in
+ * that table (e.g. 3, 4, etc). If a string is passed it can
+ * either be an exception name (e.g. "HierarchyRequestError",
+ * "WrongDocumentError") or the name of the corresponding error
+ * code (e.g. "``HIERARCHY_REQUEST_ERR``", "``WRONG_DOCUMENT_ERR``").
+ * @param {Function} descriptionOrFunc - The function expected to
+ * throw (if the exception comes from another global), or the
+ * optional description of the condition being tested (if the
+ * exception comes from the current global).
+ * @param {string} [description] - Description of the condition
+ * being tested (if the exception comes from another global).
+ *
+ */
+ function assert_throws_dom(type, funcOrConstructor, descriptionOrFunc, maybeDescription)
+ {
+ let constructor, func, description;
+ if (funcOrConstructor.name === "DOMException") {
+ constructor = funcOrConstructor;
+ func = descriptionOrFunc;
+ description = maybeDescription;
+ } else {
+ constructor = self.DOMException;
+ func = funcOrConstructor;
+ description = descriptionOrFunc;
+ assert(maybeDescription === undefined,
+ "Too many args pased to no-constructor version of assert_throws_dom");
+ }
+ assert_throws_dom_impl(type, func, description, "assert_throws_dom", constructor)
+ }
+ expose_assert(assert_throws_dom, "assert_throws_dom");
+
+ /**
+ * Similar to assert_throws_dom but allows specifying the assertion type
+ * (assert_throws_dom or promise_rejects_dom, in practice). The
+ * "constructor" argument must be the DOMException constructor from the
+ * global we expect the exception to come from.
+ */
+ function assert_throws_dom_impl(type, func, description, assertion_type, constructor)
+ {
+ try {
+ func.call(this);
+ assert(false, assertion_type, description,
+ "${func} did not throw", {func:func});
+ } catch (e) {
+ if (e instanceof AssertionError) {
+ throw e;
+ }
+
+ // Basic sanity-checks on the thrown exception.
+ assert(typeof e === "object",
+ assertion_type, description,
+ "${func} threw ${e} with type ${type}, not an object",
+ {func:func, e:e, type:typeof e});
+
+ assert(e !== null,
+ assertion_type, description,
+ "${func} threw null, not an object",
+ {func:func});
+
+ // Sanity-check our type
+ assert(typeof type == "number" ||
+ typeof type == "string",
+ assertion_type, description,
+ "${type} is not a number or string",
+ {type:type});
+
+ var codename_name_map = {
+ INDEX_SIZE_ERR: 'IndexSizeError',
+ HIERARCHY_REQUEST_ERR: 'HierarchyRequestError',
+ WRONG_DOCUMENT_ERR: 'WrongDocumentError',
+ INVALID_CHARACTER_ERR: 'InvalidCharacterError',
+ NO_MODIFICATION_ALLOWED_ERR: 'NoModificationAllowedError',
+ NOT_FOUND_ERR: 'NotFoundError',
+ NOT_SUPPORTED_ERR: 'NotSupportedError',
+ INUSE_ATTRIBUTE_ERR: 'InUseAttributeError',
+ INVALID_STATE_ERR: 'InvalidStateError',
+ SYNTAX_ERR: 'SyntaxError',
+ INVALID_MODIFICATION_ERR: 'InvalidModificationError',
+ NAMESPACE_ERR: 'NamespaceError',
+ INVALID_ACCESS_ERR: 'InvalidAccessError',
+ TYPE_MISMATCH_ERR: 'TypeMismatchError',
+ SECURITY_ERR: 'SecurityError',
+ NETWORK_ERR: 'NetworkError',
+ ABORT_ERR: 'AbortError',
+ URL_MISMATCH_ERR: 'URLMismatchError',
+ QUOTA_EXCEEDED_ERR: 'QuotaExceededError',
+ TIMEOUT_ERR: 'TimeoutError',
+ INVALID_NODE_TYPE_ERR: 'InvalidNodeTypeError',
+ DATA_CLONE_ERR: 'DataCloneError'
+ };
+
+ var name_code_map = {
+ IndexSizeError: 1,
+ HierarchyRequestError: 3,
+ WrongDocumentError: 4,
+ InvalidCharacterError: 5,
+ NoModificationAllowedError: 7,
+ NotFoundError: 8,
+ NotSupportedError: 9,
+ InUseAttributeError: 10,
+ InvalidStateError: 11,
+ SyntaxError: 12,
+ InvalidModificationError: 13,
+ NamespaceError: 14,
+ InvalidAccessError: 15,
+ TypeMismatchError: 17,
+ SecurityError: 18,
+ NetworkError: 19,
+ AbortError: 20,
+ URLMismatchError: 21,
+ QuotaExceededError: 22,
+ TimeoutError: 23,
+ InvalidNodeTypeError: 24,
+ DataCloneError: 25,
+
+ EncodingError: 0,
+ NotReadableError: 0,
+ UnknownError: 0,
+ ConstraintError: 0,
+ DataError: 0,
+ TransactionInactiveError: 0,
+ ReadOnlyError: 0,
+ VersionError: 0,
+ OperationError: 0,
+ NotAllowedError: 0,
+ OptOutError: 0
+ };
+
+ var code_name_map = {};
+ for (var key in name_code_map) {
+ if (name_code_map[key] > 0) {
+ code_name_map[name_code_map[key]] = key;
+ }
+ }
+
+ var required_props = {};
+ var name;
+
+ if (typeof type === "number") {
+ if (type === 0) {
+ throw new AssertionError('Test bug: ambiguous DOMException code 0 passed to assert_throws_dom()');
+ } else if (!(type in code_name_map)) {
+ throw new AssertionError('Test bug: unrecognized DOMException code "' + type + '" passed to assert_throws_dom()');
+ }
+ name = code_name_map[type];
+ required_props.code = type;
+ } else if (typeof type === "string") {
+ name = type in codename_name_map ? codename_name_map[type] : type;
+ if (!(name in name_code_map)) {
+ throw new AssertionError('Test bug: unrecognized DOMException code name or name "' + type + '" passed to assert_throws_dom()');
+ }
+
+ required_props.code = name_code_map[name];
+ }
+
+ if (required_props.code === 0 ||
+ ("name" in e &&
+ e.name !== e.name.toUpperCase() &&
+ e.name !== "DOMException")) {
+ // New style exception: also test the name property.
+ required_props.name = name;
+ }
+
+ for (var prop in required_props) {
+ assert(prop in e && e[prop] == required_props[prop],
+ assertion_type, description,
+ "${func} threw ${e} that is not a DOMException " + type + ": property ${prop} is equal to ${actual}, expected ${expected}",
+ {func:func, e:e, prop:prop, actual:e[prop], expected:required_props[prop]});
+ }
+
+ // Check that the exception is from the right global. This check is last
+ // so more specific, and more informative, checks on the properties can
+ // happen in case a totally incorrect exception is thrown.
+ assert(e.constructor === constructor,
+ assertion_type, description,
+ "${func} threw an exception from the wrong global",
+ {func});
+
+ }
+ }
+
+ /**
+ * Assert the provided value is thrown.
+ *
+ * @param {value} exception The expected exception.
+ * @param {Function} func Function which should throw.
+ * @param {string} [description] Error description for the case that the error is not thrown.
+ */
+ function assert_throws_exactly(exception, func, description)
+ {
+ assert_throws_exactly_impl(exception, func, description,
+ "assert_throws_exactly");
+ }
+ expose_assert(assert_throws_exactly, "assert_throws_exactly");
+
+ /**
+ * Like assert_throws_exactly but allows specifying the assertion type
+ * (assert_throws_exactly or promise_rejects_exactly, in practice).
+ */
+ function assert_throws_exactly_impl(exception, func, description,
+ assertion_type)
+ {
+ try {
+ func.call(this);
+ assert(false, assertion_type, description,
+ "${func} did not throw", {func:func});
+ } catch (e) {
+ if (e instanceof AssertionError) {
+ throw e;
+ }
+
+ assert(same_value(e, exception), assertion_type, description,
+ "${func} threw ${e} but we expected it to throw ${exception}",
+ {func:func, e:e, exception:exception});
+ }
+ }
+
+ /**
+ * Asserts if called. Used to ensure that a specific codepath is
+ * not taken e.g. that an error event isn't fired.
+ *
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_unreached(description) {
+ assert(false, "assert_unreached", description,
+ "Reached unreachable code");
+ }
+ expose_assert(assert_unreached, "assert_unreached");
+
+ /**
+ * @callback AssertFunc
+ * @param {Any} actual
+ * @param {Any} expected
+ * @param {Any[]} args
+ */
+
+ /**
+ * Asserts that ``actual`` matches at least one value of ``expected``
+ * according to a comparison defined by ``assert_func``.
+ *
+ * Note that tests with multiple allowed pass conditions are bad
+ * practice unless the spec specifically allows multiple
+ * behaviours. Test authors should not use this method simply to
+ * hide UA bugs.
+ *
+ * @param {AssertFunc} assert_func - Function to compare actual
+ * and expected. It must throw when the comparison fails and
+ * return when the comparison passes.
+ * @param {Any} actual - Test value.
+ * @param {Array} expected_array - Array of possible expected values.
+ * @param {Any[]} args - Additional arguments to pass to ``assert_func``.
+ */
+ function assert_any(assert_func, actual, expected_array, ...args)
+ {
+ var errors = [];
+ var passed = false;
+ forEach(expected_array,
+ function(expected)
+ {
+ try {
+ assert_func.apply(this, [actual, expected].concat(args));
+ passed = true;
+ } catch (e) {
+ errors.push(e.message);
+ }
+ });
+ if (!passed) {
+ throw new AssertionError(errors.join("\n\n"));
+ }
+ }
+ // FIXME: assert_any cannot use expose_assert, because assert_wrapper does
+ // not support nested assert calls (e.g. to assert_func). We need to
+ // support bypassing assert_wrapper for the inner asserts here.
+ expose(assert_any, "assert_any");
+
+ /**
+ * Assert that a feature is implemented, based on a 'truthy' condition.
+ *
+ * This function should be used to early-exit from tests in which there is
+ * no point continuing without support for a non-optional spec or spec
+ * feature. For example:
+ *
+ * assert_implements(window.Foo, 'Foo is not supported');
+ *
+ * @param {object} condition The truthy value to test
+ * @param {string} [description] Error description for the case that the condition is not truthy.
+ */
+ function assert_implements(condition, description) {
+ assert(!!condition, "assert_implements", description);
+ }
+ expose_assert(assert_implements, "assert_implements")
+
+ /**
+ * Assert that an optional feature is implemented, based on a 'truthy' condition.
+ *
+ * This function should be used to early-exit from tests in which there is
+ * no point continuing without support for an explicitly optional spec or
+ * spec feature. For example:
+ *
+ * assert_implements_optional(video.canPlayType("video/webm"),
+ * "webm video playback not supported");
+ *
+ * @param {object} condition The truthy value to test
+ * @param {string} [description] Error description for the case that the condition is not truthy.
+ */
+ function assert_implements_optional(condition, description) {
+ if (!condition) {
+ throw new OptionalFeatureUnsupportedError(description);
+ }
+ }
+ expose_assert(assert_implements_optional, "assert_implements_optional");
+
+ /**
+ * @class
+ *
+ * A single subtest. A Test is not constructed directly but via the
+ * :js:func:`test`, :js:func:`async_test` or :js:func:`promise_test` functions.
+ *
+ * @param {string} name - This must be unique in a given file and must be
+ * invariant between runs.
+ *
+ */
+ function Test(name, properties)
+ {
+ if (tests.file_is_test && tests.tests.length) {
+ throw new Error("Tried to create a test with file_is_test");
+ }
+ /** The test name. */
+ this.name = name;
+
+ this.phase = (tests.is_aborted || tests.phase === tests.phases.COMPLETE) ?
+ this.phases.COMPLETE : this.phases.INITIAL;
+
+ /** The test status code.*/
+ this.status = this.NOTRUN;
+ this.timeout_id = null;
+ this.index = null;
+
+ this.properties = properties || {};
+ this.timeout_length = settings.test_timeout;
+ if (this.timeout_length !== null) {
+ this.timeout_length *= tests.timeout_multiplier;
+ }
+
+ /** A message indicating the reason for test failure. */
+ this.message = null;
+ /** Stack trace in case of failure. */
+ this.stack = null;
+
+ this.steps = [];
+ this._is_promise_test = false;
+
+ this.cleanup_callbacks = [];
+ this._user_defined_cleanup_count = 0;
+ this._done_callbacks = [];
+
+ if (typeof AbortController === "function") {
+ this._abortController = new AbortController();
+ }
+
+ // Tests declared following harness completion are likely an indication
+ // of a programming error, but they cannot be reported
+ // deterministically.
+ if (tests.phase === tests.phases.COMPLETE) {
+ return;
+ }
+
+ tests.push(this);
+ }
+
+ /**
+ * Enum of possible test statuses.
+ *
+ * :values:
+ * - ``PASS``
+ * - ``FAIL``
+ * - ``TIMEOUT``
+ * - ``NOTRUN``
+ * - ``PRECONDITION_FAILED``
+ */
+ Test.statuses = {
+ PASS:0,
+ FAIL:1,
+ TIMEOUT:2,
+ NOTRUN:3,
+ PRECONDITION_FAILED:4
+ };
+
+ Test.prototype = merge({}, Test.statuses);
+
+ Test.prototype.phases = {
+ INITIAL:0,
+ STARTED:1,
+ HAS_RESULT:2,
+ CLEANING:3,
+ COMPLETE:4
+ };
+
+ Test.prototype.status_formats = {
+ 0: "Pass",
+ 1: "Fail",
+ 2: "Timeout",
+ 3: "Not Run",
+ 4: "Optional Feature Unsupported",
+ }
+
+ Test.prototype.format_status = function() {
+ return this.status_formats[this.status];
+ }
+
+ Test.prototype.structured_clone = function()
+ {
+ if (!this._structured_clone) {
+ var msg = this.message;
+ msg = msg ? String(msg) : msg;
+ this._structured_clone = merge({
+ name:String(this.name),
+ properties:merge({}, this.properties),
+ phases:merge({}, this.phases)
+ }, Test.statuses);
+ }
+ this._structured_clone.status = this.status;
+ this._structured_clone.message = this.message;
+ this._structured_clone.stack = this.stack;
+ this._structured_clone.index = this.index;
+ this._structured_clone.phase = this.phase;
+ return this._structured_clone;
+ };
+
+ /**
+ * Run a single step of an ongoing test.
+ *
+ * @param {string} func - Callback function to run as a step. If
+ * this throws an :js:func:`AssertionError`, or any other
+ * exception, the :js:class:`Test` status is set to ``FAIL``.
+ * @param {Object} [this_obj] - The object to use as the this
+ * value when calling ``func``. Defaults to the :js:class:`Test` object.
+ */
+ Test.prototype.step = function(func, this_obj)
+ {
+ if (this.phase > this.phases.STARTED) {
+ return;
+ }
+
+ if (settings.debug && this.phase !== this.phases.STARTED) {
+ console.log("TEST START", this.name);
+ }
+ this.phase = this.phases.STARTED;
+ //If we don't get a result before the harness times out that will be a test timeout
+ this.set_status(this.TIMEOUT, "Test timed out");
+
+ tests.started = true;
+ tests.current_test = this;
+ tests.notify_test_state(this);
+
+ if (this.timeout_id === null) {
+ this.set_timeout();
+ }
+
+ this.steps.push(func);
+
+ if (arguments.length === 1) {
+ this_obj = this;
+ }
+
+ if (settings.debug) {
+ console.debug("TEST STEP", this.name);
+ }
+
+ try {
+ return func.apply(this_obj, Array.prototype.slice.call(arguments, 2));
+ } catch (e) {
+ if (this.phase >= this.phases.HAS_RESULT) {
+ return;
+ }
+ var status = e instanceof OptionalFeatureUnsupportedError ? this.PRECONDITION_FAILED : this.FAIL;
+ var message = String((typeof e === "object" && e !== null) ? e.message : e);
+ var stack = e.stack ? e.stack : null;
+
+ this.set_status(status, message, stack);
+ this.phase = this.phases.HAS_RESULT;
+ this.done();
+ } finally {
+ this.current_test = null;
+ }
+ };
+
+ /**
+ * Wrap a function so that it runs as a step of the current test.
+ *
+ * This allows creating a callback function that will run as a
+ * test step.
+ *
+ * @example
+ * let t = async_test("Example");
+ * onload = t.step_func(e => {
+ * assert_equals(e.name, "load");
+ * // Mark the test as complete.
+ * t.done();
+ * })
+ *
+ * @param {string} func - Function to run as a step. If this
+ * throws an :js:func:`AssertionError`, or any other exception,
+ * the :js:class:`Test` status is set to ``FAIL``.
+ * @param {Object} [this_obj] - The object to use as the this
+ * value when calling ``func``. Defaults to the :js:class:`Test` object.
+ */
+ Test.prototype.step_func = function(func, this_obj)
+ {
+ var test_this = this;
+
+ if (arguments.length === 1) {
+ this_obj = test_this;
+ }
+
+ return function()
+ {
+ return test_this.step.apply(test_this, [func, this_obj].concat(
+ Array.prototype.slice.call(arguments)));
+ };
+ };
+
+ /**
+ * Wrap a function so that it runs as a step of the current test,
+ * and automatically marks the test as complete if the function
+ * returns without error.
+ *
+ * @param {string} func - Function to run as a step. If this
+ * throws an :js:func:`AssertionError`, or any other exception,
+ * the :js:class:`Test` status is set to ``FAIL``. If it returns
+ * without error the status is set to ``PASS``.
+ * @param {Object} [this_obj] - The object to use as the this
+ * value when calling `func`. Defaults to the :js:class:`Test` object.
+ */
+ Test.prototype.step_func_done = function(func, this_obj)
+ {
+ var test_this = this;
+
+ if (arguments.length === 1) {
+ this_obj = test_this;
+ }
+
+ return function()
+ {
+ if (func) {
+ test_this.step.apply(test_this, [func, this_obj].concat(
+ Array.prototype.slice.call(arguments)));
+ }
+ test_this.done();
+ };
+ };
+
+ /**
+ * Return a function that automatically sets the current test to
+ * ``FAIL`` if it's called.
+ *
+ * @param {string} [description] - Error message to add to assert
+ * in case of failure.
+ *
+ */
+ Test.prototype.unreached_func = function(description)
+ {
+ return this.step_func(function() {
+ assert_unreached(description);
+ });
+ };
+
+ /**
+ * Run a function as a step of the test after a given timeout.
+ *
+ * This multiplies the timeout by the global timeout multiplier to
+ * account for the expected execution speed of the current test
+ * environment. For example ``test.step_timeout(f, 2000)`` with a
+ * timeout multiplier of 2 will wait for 4000ms before calling ``f``.
+ *
+ * In general it's encouraged to use :js:func:`Test.step_wait` or
+ * :js:func:`step_wait_func` in preference to this function where possible,
+ * as they provide better test performance.
+ *
+ * @param {Function} func - Function to run as a test
+ * step.
+ * @param {number} timeout - Time in ms to wait before running the
+ * test step. The actual wait time is ``timeout`` x
+ * ``timeout_multiplier``.
+ *
+ */
+ Test.prototype.step_timeout = function(func, timeout) {
+ var test_this = this;
+ var args = Array.prototype.slice.call(arguments, 2);
+ return setTimeout(this.step_func(function() {
+ return func.apply(test_this, args);
+ }), timeout * tests.timeout_multiplier);
+ };
+
+ /**
+ * Poll for a function to return true, and call a callback
+ * function once it does, or assert if a timeout is
+ * reached. This is preferred over a simple step_timeout
+ * whenever possible since it allows the timeout to be longer
+ * to reduce intermittents without compromising test execution
+ * speed when the condition is quickly met.
+ *
+ * @param {Function} cond A function taking no arguments and
+ * returning a boolean. The callback is called
+ * when this function returns true.
+ * @param {Function} func A function taking no arguments to call once
+ * the condition is met.
+ * @param {string} [description] Error message to add to assert in case of
+ * failure.
+ * @param {number} timeout Timeout in ms. This is multiplied by the global
+ * timeout_multiplier
+ * @param {number} interval Polling interval in ms
+ *
+ */
+ Test.prototype.step_wait_func = function(cond, func, description,
+ timeout=3000, interval=100) {
+ var timeout_full = timeout * tests.timeout_multiplier;
+ var remaining = Math.ceil(timeout_full / interval);
+ var test_this = this;
+
+ var wait_for_inner = test_this.step_func(() => {
+ if (cond()) {
+ func();
+ } else {
+ if(remaining === 0) {
+ assert(false, "step_wait_func", description,
+ "Timed out waiting on condition");
+ }
+ remaining--;
+ setTimeout(wait_for_inner, interval);
+ }
+ });
+
+ wait_for_inner();
+ };
+
+ /**
+ * Poll for a function to return true, and invoke a callback
+ * followed by this.done() once it does, or assert if a timeout
+ * is reached. This is preferred over a simple step_timeout
+ * whenever possible since it allows the timeout to be longer
+ * to reduce intermittents without compromising test execution speed
+ * when the condition is quickly met.
+ *
+ * @example
+ * async_test(t => {
+ * const popup = window.open("resources/coop-coep.py?coop=same-origin&coep=&navigate=about:blank");
+ * t.add_cleanup(() => popup.close());
+ * assert_equals(window, popup.opener);
+ *
+ * popup.onload = t.step_func(() => {
+ * assert_true(popup.location.href.endsWith("&navigate=about:blank"));
+ * // Use step_wait_func_done as about:blank cannot message back.
+ * t.step_wait_func_done(() => popup.location.href === "about:blank");
+ * });
+ * }, "Navigating a popup to about:blank");
+ *
+ * @param {Function} cond A function taking no arguments and
+ * returning a boolean. The callback is called
+ * when this function returns true.
+ * @param {Function} func A function taking no arguments to call once
+ * the condition is met.
+ * @param {string} [description] Error message to add to assert in case of
+ * failure.
+ * @param {number} timeout Timeout in ms. This is multiplied by the global
+ * timeout_multiplier
+ * @param {number} interval Polling interval in ms
+ *
+ */
+ Test.prototype.step_wait_func_done = function(cond, func, description,
+ timeout=3000, interval=100) {
+ this.step_wait_func(cond, () => {
+ if (func) {
+ func();
+ }
+ this.done();
+ }, description, timeout, interval);
+ };
+
+ /**
+ * Poll for a function to return true, and resolve a promise
+ * once it does, or assert if a timeout is reached. This is
+ * preferred over a simple step_timeout whenever possible
+ * since it allows the timeout to be longer to reduce
+ * intermittents without compromising test execution speed
+ * when the condition is quickly met.
+ *
+ * @example
+ * promise_test(async t => {
+ * // …
+ * await t.step_wait(() => frame.contentDocument === null, "Frame navigated to a cross-origin document");
+ * // …
+ * }, "");
+ *
+ * @param {Function} cond A function taking no arguments and
+ * returning a boolean.
+ * @param {string} [description] Error message to add to assert in case of
+ * failure.
+ * @param {number} timeout Timeout in ms. This is multiplied by the global
+ * timeout_multiplier
+ * @param {number} interval Polling interval in ms
+ * @returns {Promise} Promise resolved once cond is met.
+ *
+ */
+ Test.prototype.step_wait = function(cond, description, timeout=3000, interval=100) {
+ return new Promise(resolve => {
+ this.step_wait_func(cond, resolve, description, timeout, interval);
+ });
+ }
+
+ /*
+ * Private method for registering cleanup functions. `testharness.js`
+ * internals should use this method instead of the public `add_cleanup`
+ * method in order to hide implementation details from the harness status
+ * message in the case errors.
+ */
+ Test.prototype._add_cleanup = function(callback) {
+ this.cleanup_callbacks.push(callback);
+ };
+
+ /**
+ * Schedule a function to be run after the test result is known, regardless
+ * of passing or failing state.
+ *
+ * The behavior of this function will not
+ * influence the result of the test, but if an exception is thrown, the
+ * test harness will report an error.
+ *
+ * @param {Function} callback - The cleanup function to run. This
+ * is called with no arguments.
+ */
+ Test.prototype.add_cleanup = function(callback) {
+ this._user_defined_cleanup_count += 1;
+ this._add_cleanup(callback);
+ };
+
+ Test.prototype.set_timeout = function()
+ {
+ if (this.timeout_length !== null) {
+ var this_obj = this;
+ this.timeout_id = setTimeout(function()
+ {
+ this_obj.timeout();
+ }, this.timeout_length);
+ }
+ };
+
+ Test.prototype.set_status = function(status, message, stack)
+ {
+ this.status = status;
+ this.message = message;
+ this.stack = stack ? stack : null;
+ };
+
+ /**
+ * Manually set the test status to ``TIMEOUT``.
+ */
+ Test.prototype.timeout = function()
+ {
+ this.timeout_id = null;
+ this.set_status(this.TIMEOUT, "Test timed out");
+ this.phase = this.phases.HAS_RESULT;
+ this.done();
+ };
+
+ /**
+ * Manually set the test status to ``TIMEOUT``.
+ *
+ * Alias for `Test.timeout <#Test.timeout>`_.
+ */
+ Test.prototype.force_timeout = function() {
+ return this.timeout();
+ };
+
+ /**
+ * Mark the test as complete.
+ *
+ * This sets the test status to ``PASS`` if no other status was
+ * already recorded. Any subsequent attempts to run additional
+ * test steps will be ignored.
+ *
+ * After setting the test status any test cleanup functions will
+ * be run.
+ */
+ Test.prototype.done = function()
+ {
+ if (this.phase >= this.phases.CLEANING) {
+ return;
+ }
+
+ if (this.phase <= this.phases.STARTED) {
+ this.set_status(this.PASS, null);
+ }
+
+ if (global_scope.clearTimeout) {
+ clearTimeout(this.timeout_id);
+ }
+
+ if (settings.debug) {
+ console.log("TEST DONE",
+ this.status,
+ this.name);
+ }
+
+ this.cleanup();
+ };
+
+ function add_test_done_callback(test, callback)
+ {
+ if (test.phase === test.phases.COMPLETE) {
+ callback();
+ return;
+ }
+
+ test._done_callbacks.push(callback);
+ }
+
+ /*
+ * Invoke all specified cleanup functions. If one or more produce an error,
+ * the context is in an unpredictable state, so all further testing should
+ * be cancelled.
+ */
+ Test.prototype.cleanup = function() {
+ var errors = [];
+ var bad_value_count = 0;
+ function on_error(e) {
+ errors.push(e);
+ // Abort tests immediately so that tests declared within subsequent
+ // cleanup functions are not run.
+ tests.abort();
+ }
+ var this_obj = this;
+ var results = [];
+
+ this.phase = this.phases.CLEANING;
+
+ if (this._abortController) {
+ this._abortController.abort("Test cleanup");
+ }
+
+ forEach(this.cleanup_callbacks,
+ function(cleanup_callback) {
+ var result;
+
+ try {
+ result = cleanup_callback();
+ } catch (e) {
+ on_error(e);
+ return;
+ }
+
+ if (!is_valid_cleanup_result(this_obj, result)) {
+ bad_value_count += 1;
+ // Abort tests immediately so that tests declared
+ // within subsequent cleanup functions are not run.
+ tests.abort();
+ }
+
+ results.push(result);
+ });
+
+ if (!this._is_promise_test) {
+ cleanup_done(this_obj, errors, bad_value_count);
+ } else {
+ all_async(results,
+ function(result, done) {
+ if (result && typeof result.then === "function") {
+ result
+ .then(null, on_error)
+ .then(done);
+ } else {
+ done();
+ }
+ },
+ function() {
+ cleanup_done(this_obj, errors, bad_value_count);
+ });
+ }
+ };
+
+ /*
+ * Determine if the return value of a cleanup function is valid for a given
+ * test. Any test may return the value `undefined`. Tests created with
+ * `promise_test` may alternatively return "thenable" object values.
+ */
+ function is_valid_cleanup_result(test, result) {
+ if (result === undefined) {
+ return true;
+ }
+
+ if (test._is_promise_test) {
+ return result && typeof result.then === "function";
+ }
+
+ return false;
+ }
+
+ function cleanup_done(test, errors, bad_value_count) {
+ if (errors.length || bad_value_count) {
+ var total = test._user_defined_cleanup_count;
+
+ tests.status.status = tests.status.ERROR;
+ tests.status.stack = null;
+ tests.status.message = "Test named '" + test.name +
+ "' specified " + total +
+ " 'cleanup' function" + (total > 1 ? "s" : "");
+
+ if (errors.length) {
+ tests.status.message += ", and " + errors.length + " failed";
+ tests.status.stack = ((typeof errors[0] === "object" &&
+ errors[0].hasOwnProperty("stack")) ?
+ errors[0].stack : null);
+ }
+
+ if (bad_value_count) {
+ var type = test._is_promise_test ?
+ "non-thenable" : "non-undefined";
+ tests.status.message += ", and " + bad_value_count +
+ " returned a " + type + " value";
+ }
+
+ tests.status.message += ".";
+ }
+
+ test.phase = test.phases.COMPLETE;
+ tests.result(test);
+ forEach(test._done_callbacks,
+ function(callback) {
+ callback();
+ });
+ test._done_callbacks.length = 0;
+ }
+
+ /**
+ * Gives an AbortSignal that will be aborted when the test finishes.
+ */
+ Test.prototype.get_signal = function() {
+ if (!this._abortController) {
+ throw new Error("AbortController is not supported in this browser");
+ }
+ return this._abortController.signal;
+ }
+
+ /**
+ * A RemoteTest object mirrors a Test object on a remote worker. The
+ * associated RemoteWorker updates the RemoteTest object in response to
+ * received events. In turn, the RemoteTest object replicates these events
+ * on the local document. This allows listeners (test result reporting
+ * etc..) to transparently handle local and remote events.
+ */
+ function RemoteTest(clone) {
+ var this_obj = this;
+ Object.keys(clone).forEach(
+ function(key) {
+ this_obj[key] = clone[key];
+ });
+ this.index = null;
+ this.phase = this.phases.INITIAL;
+ this.update_state_from(clone);
+ this._done_callbacks = [];
+ tests.push(this);
+ }
+
+ RemoteTest.prototype.structured_clone = function() {
+ var clone = {};
+ Object.keys(this).forEach(
+ (function(key) {
+ var value = this[key];
+ // `RemoteTest` instances are responsible for managing
+ // their own "done" callback functions, so those functions
+ // are not relevant in other execution contexts. Because of
+ // this (and because Function values cannot be serialized
+ // for cross-realm transmittance), the property should not
+ // be considered when cloning instances.
+ if (key === '_done_callbacks' ) {
+ return;
+ }
+
+ if (typeof value === "object" && value !== null) {
+ clone[key] = merge({}, value);
+ } else {
+ clone[key] = value;
+ }
+ }).bind(this));
+ clone.phases = merge({}, this.phases);
+ return clone;
+ };
+
+ /**
+ * `RemoteTest` instances are objects which represent tests running in
+ * another realm. They do not define "cleanup" functions (if necessary,
+ * such functions are defined on the associated `Test` instance within the
+ * external realm). However, `RemoteTests` may have "done" callbacks (e.g.
+ * as attached by the `Tests` instance responsible for tracking the overall
+ * test status in the parent realm). The `cleanup` method delegates to
+ * `done` in order to ensure that such callbacks are invoked following the
+ * completion of the `RemoteTest`.
+ */
+ RemoteTest.prototype.cleanup = function() {
+ this.done();
+ };
+ RemoteTest.prototype.phases = Test.prototype.phases;
+ RemoteTest.prototype.update_state_from = function(clone) {
+ this.status = clone.status;
+ this.message = clone.message;
+ this.stack = clone.stack;
+ if (this.phase === this.phases.INITIAL) {
+ this.phase = this.phases.STARTED;
+ }
+ };
+ RemoteTest.prototype.done = function() {
+ this.phase = this.phases.COMPLETE;
+
+ forEach(this._done_callbacks,
+ function(callback) {
+ callback();
+ });
+ }
+
+ RemoteTest.prototype.format_status = function() {
+ return Test.prototype.status_formats[this.status];
+ }
+
+ /*
+ * A RemoteContext listens for test events from a remote test context, such
+ * as another window or a worker. These events are then used to construct
+ * and maintain RemoteTest objects that mirror the tests running in the
+ * remote context.
+ *
+ * An optional third parameter can be used as a predicate to filter incoming
+ * MessageEvents.
+ */
+ function RemoteContext(remote, message_target, message_filter) {
+ this.running = true;
+ this.started = false;
+ this.tests = new Array();
+ this.early_exception = null;
+
+ var this_obj = this;
+ // If remote context is cross origin assigning to onerror is not
+ // possible, so silently catch those errors.
+ try {
+ remote.onerror = function(error) { this_obj.remote_error(error); };
+ } catch (e) {
+ // Ignore.
+ }
+
+ // Keeping a reference to the remote object and the message handler until
+ // remote_done() is seen prevents the remote object and its message channel
+ // from going away before all the messages are dispatched.
+ this.remote = remote;
+ this.message_target = message_target;
+ this.message_handler = function(message) {
+ var passesFilter = !message_filter || message_filter(message);
+ // The reference to the `running` property in the following
+ // condition is unnecessary because that value is only set to
+ // `false` after the `message_handler` function has been
+ // unsubscribed.
+ // TODO: Simplify the condition by removing the reference.
+ if (this_obj.running && message.data && passesFilter &&
+ (message.data.type in this_obj.message_handlers)) {
+ this_obj.message_handlers[message.data.type].call(this_obj, message.data);
+ }
+ };
+
+ if (self.Promise) {
+ this.done = new Promise(function(resolve) {
+ this_obj.doneResolve = resolve;
+ });
+ }
+
+ this.message_target.addEventListener("message", this.message_handler);
+ }
+
+ RemoteContext.prototype.remote_error = function(error) {
+ if (error.preventDefault) {
+ error.preventDefault();
+ }
+
+ // Defer interpretation of errors until the testing protocol has
+ // started and the remote test's `allow_uncaught_exception` property
+ // is available.
+ if (!this.started) {
+ this.early_exception = error;
+ } else if (!this.allow_uncaught_exception) {
+ this.report_uncaught(error);
+ }
+ };
+
+ RemoteContext.prototype.report_uncaught = function(error) {
+ var message = error.message || String(error);
+ var filename = (error.filename ? " " + error.filename: "");
+ // FIXME: Display remote error states separately from main document
+ // error state.
+ tests.set_status(tests.status.ERROR,
+ "Error in remote" + filename + ": " + message,
+ error.stack);
+ };
+
+ RemoteContext.prototype.start = function(data) {
+ this.started = true;
+ this.allow_uncaught_exception = data.properties.allow_uncaught_exception;
+
+ if (this.early_exception && !this.allow_uncaught_exception) {
+ this.report_uncaught(this.early_exception);
+ }
+ };
+
+ RemoteContext.prototype.test_state = function(data) {
+ var remote_test = this.tests[data.test.index];
+ if (!remote_test) {
+ remote_test = new RemoteTest(data.test);
+ this.tests[data.test.index] = remote_test;
+ }
+ remote_test.update_state_from(data.test);
+ tests.notify_test_state(remote_test);
+ };
+
+ RemoteContext.prototype.test_done = function(data) {
+ var remote_test = this.tests[data.test.index];
+ remote_test.update_state_from(data.test);
+ remote_test.done();
+ tests.result(remote_test);
+ };
+
+ RemoteContext.prototype.remote_done = function(data) {
+ if (tests.status.status === null &&
+ data.status.status !== data.status.OK) {
+ tests.set_status(data.status.status, data.status.message, data.status.stack);
+ }
+
+ for (let assert of data.asserts) {
+ var record = new AssertRecord();
+ record.assert_name = assert.assert_name;
+ record.args = assert.args;
+ record.test = assert.test != null ? this.tests[assert.test.index] : null;
+ record.status = assert.status;
+ record.stack = assert.stack;
+ tests.asserts_run.push(record);
+ }
+
+ this.message_target.removeEventListener("message", this.message_handler);
+ this.running = false;
+
+ // If remote context is cross origin assigning to onerror is not
+ // possible, so silently catch those errors.
+ try {
+ this.remote.onerror = null;
+ } catch (e) {
+ // Ignore.
+ }
+
+ this.remote = null;
+ this.message_target = null;
+ if (this.doneResolve) {
+ this.doneResolve();
+ }
+
+ if (tests.all_done()) {
+ tests.complete();
+ }
+ };
+
+ RemoteContext.prototype.message_handlers = {
+ start: RemoteContext.prototype.start,
+ test_state: RemoteContext.prototype.test_state,
+ result: RemoteContext.prototype.test_done,
+ complete: RemoteContext.prototype.remote_done
+ };
+
+ /**
+ * @class
+ * Status of the overall harness
+ */
+ function TestsStatus()
+ {
+ /** The status code */
+ this.status = null;
+ /** Message in case of failure */
+ this.message = null;
+ /** Stack trace in case of an exception. */
+ this.stack = null;
+ }
+
+ /**
+ * Enum of possible harness statuses.
+ *
+ * :values:
+ * - ``OK``
+ * - ``ERROR``
+ * - ``TIMEOUT``
+ * - ``PRECONDITION_FAILED``
+ */
+ TestsStatus.statuses = {
+ OK:0,
+ ERROR:1,
+ TIMEOUT:2,
+ PRECONDITION_FAILED:3
+ };
+
+ TestsStatus.prototype = merge({}, TestsStatus.statuses);
+
+ TestsStatus.prototype.formats = {
+ 0: "OK",
+ 1: "Error",
+ 2: "Timeout",
+ 3: "Optional Feature Unsupported"
+ };
+
+ TestsStatus.prototype.structured_clone = function()
+ {
+ if (!this._structured_clone) {
+ var msg = this.message;
+ msg = msg ? String(msg) : msg;
+ this._structured_clone = merge({
+ status:this.status,
+ message:msg,
+ stack:this.stack
+ }, TestsStatus.statuses);
+ }
+ return this._structured_clone;
+ };
+
+ TestsStatus.prototype.format_status = function() {
+ return this.formats[this.status];
+ };
+
+ /**
+ * @class
+ * Record of an assert that ran.
+ *
+ * @param {Test} test - The test which ran the assert.
+ * @param {string} assert_name - The function name of the assert.
+ * @param {Any} args - The arguments passed to the assert function.
+ */
+ function AssertRecord(test, assert_name, args = []) {
+ /** Name of the assert that ran */
+ this.assert_name = assert_name;
+ /** Test that ran the assert */
+ this.test = test;
+ // Avoid keeping complex objects alive
+ /** Stringification of the arguments that were passed to the assert function */
+ this.args = args.map(x => format_value(x).replace(/\n/g, " "));
+ /** Status of the assert */
+ this.status = null;
+ }
+
+ AssertRecord.prototype.structured_clone = function() {
+ return {
+ assert_name: this.assert_name,
+ test: this.test ? this.test.structured_clone() : null,
+ args: this.args,
+ status: this.status,
+ };
+ };
+
+ function Tests()
+ {
+ this.tests = [];
+ this.num_pending = 0;
+
+ this.phases = {
+ INITIAL:0,
+ SETUP:1,
+ HAVE_TESTS:2,
+ HAVE_RESULTS:3,
+ COMPLETE:4
+ };
+ this.phase = this.phases.INITIAL;
+
+ this.properties = {};
+
+ this.wait_for_finish = false;
+ this.processing_callbacks = false;
+
+ this.allow_uncaught_exception = false;
+
+ this.file_is_test = false;
+ // This value is lazily initialized in order to avoid introducing a
+ // dependency on ECMAScript 2015 Promises to all tests.
+ this.promise_tests = null;
+ this.promise_setup_called = false;
+
+ this.timeout_multiplier = 1;
+ this.timeout_length = test_environment.test_timeout();
+ this.timeout_id = null;
+
+ this.start_callbacks = [];
+ this.test_state_callbacks = [];
+ this.test_done_callbacks = [];
+ this.all_done_callbacks = [];
+
+ this.hide_test_state = false;
+ this.pending_remotes = [];
+
+ this.current_test = null;
+ this.asserts_run = [];
+
+ // Track whether output is enabled, and thus whether or not we should
+ // track asserts.
+ //
+ // On workers we don't get properties set from testharnessreport.js, so
+ // we don't know whether or not to track asserts. To avoid the
+ // resulting performance hit, we assume we are not meant to. This means
+ // that assert tracking does not function on workers.
+ this.output = settings.output && 'document' in global_scope;
+
+ this.status = new TestsStatus();
+
+ var this_obj = this;
+
+ test_environment.add_on_loaded_callback(function() {
+ if (this_obj.all_done()) {
+ this_obj.complete();
+ }
+ });
+
+ this.set_timeout();
+ }
+
+ Tests.prototype.setup = function(func, properties)
+ {
+ if (this.phase >= this.phases.HAVE_RESULTS) {
+ return;
+ }
+
+ if (this.phase < this.phases.SETUP) {
+ this.phase = this.phases.SETUP;
+ }
+
+ this.properties = properties;
+
+ for (var p in properties) {
+ if (properties.hasOwnProperty(p)) {
+ var value = properties[p];
+ if (p == "allow_uncaught_exception") {
+ this.allow_uncaught_exception = value;
+ } else if (p == "explicit_done" && value) {
+ this.wait_for_finish = true;
+ } else if (p == "explicit_timeout" && value) {
+ this.timeout_length = null;
+ if (this.timeout_id)
+ {
+ clearTimeout(this.timeout_id);
+ }
+ } else if (p == "single_test" && value) {
+ this.set_file_is_test();
+ } else if (p == "timeout_multiplier") {
+ this.timeout_multiplier = value;
+ if (this.timeout_length) {
+ this.timeout_length *= this.timeout_multiplier;
+ }
+ } else if (p == "hide_test_state") {
+ this.hide_test_state = value;
+ } else if (p == "output") {
+ this.output = value;
+ } else if (p === "debug") {
+ settings.debug = value;
+ }
+ }
+ }
+
+ if (func) {
+ try {
+ func();
+ } catch (e) {
+ this.status.status = e instanceof OptionalFeatureUnsupportedError ? this.status.PRECONDITION_FAILED : this.status.ERROR;
+ this.status.message = String(e);
+ this.status.stack = e.stack ? e.stack : null;
+ this.complete();
+ }
+ }
+ this.set_timeout();
+ };
+
+ Tests.prototype.set_file_is_test = function() {
+ if (this.tests.length > 0) {
+ throw new Error("Tried to set file as test after creating a test");
+ }
+ this.wait_for_finish = true;
+ this.file_is_test = true;
+ // Create the test, which will add it to the list of tests
+ tests.current_test = async_test();
+ };
+
+ Tests.prototype.set_status = function(status, message, stack)
+ {
+ this.status.status = status;
+ this.status.message = message;
+ this.status.stack = stack ? stack : null;
+ };
+
+ Tests.prototype.set_timeout = function() {
+ if (global_scope.clearTimeout) {
+ var this_obj = this;
+ clearTimeout(this.timeout_id);
+ if (this.timeout_length !== null) {
+ this.timeout_id = setTimeout(function() {
+ this_obj.timeout();
+ }, this.timeout_length);
+ }
+ }
+ };
+
+ Tests.prototype.timeout = function() {
+ var test_in_cleanup = null;
+
+ if (this.status.status === null) {
+ forEach(this.tests,
+ function(test) {
+ // No more than one test is expected to be in the
+ // "CLEANUP" phase at any time
+ if (test.phase === test.phases.CLEANING) {
+ test_in_cleanup = test;
+ }
+
+ test.phase = test.phases.COMPLETE;
+ });
+
+ // Timeouts that occur while a test is in the "cleanup" phase
+ // indicate that some global state was not properly reverted. This
+ // invalidates the overall test execution, so the timeout should be
+ // reported as an error and cancel the execution of any remaining
+ // tests.
+ if (test_in_cleanup) {
+ this.status.status = this.status.ERROR;
+ this.status.message = "Timeout while running cleanup for " +
+ "test named \"" + test_in_cleanup.name + "\".";
+ tests.status.stack = null;
+ } else {
+ this.status.status = this.status.TIMEOUT;
+ }
+ }
+
+ this.complete();
+ };
+
+ Tests.prototype.end_wait = function()
+ {
+ this.wait_for_finish = false;
+ if (this.all_done()) {
+ this.complete();
+ }
+ };
+
+ Tests.prototype.push = function(test)
+ {
+ if (this.phase < this.phases.HAVE_TESTS) {
+ this.start();
+ }
+ this.num_pending++;
+ test.index = this.tests.push(test);
+ this.notify_test_state(test);
+ };
+
+ Tests.prototype.notify_test_state = function(test) {
+ var this_obj = this;
+ forEach(this.test_state_callbacks,
+ function(callback) {
+ callback(test, this_obj);
+ });
+ };
+
+ Tests.prototype.all_done = function() {
+ return (this.tests.length > 0 || this.pending_remotes.length > 0) &&
+ test_environment.all_loaded &&
+ (this.num_pending === 0 || this.is_aborted) && !this.wait_for_finish &&
+ !this.processing_callbacks &&
+ !this.pending_remotes.some(function(w) { return w.running; });
+ };
+
+ Tests.prototype.start = function() {
+ this.phase = this.phases.HAVE_TESTS;
+ this.notify_start();
+ };
+
+ Tests.prototype.notify_start = function() {
+ var this_obj = this;
+ forEach (this.start_callbacks,
+ function(callback)
+ {
+ callback(this_obj.properties);
+ });
+ };
+
+ Tests.prototype.result = function(test)
+ {
+ // If the harness has already transitioned beyond the `HAVE_RESULTS`
+ // phase, subsequent tests should not cause it to revert.
+ if (this.phase <= this.phases.HAVE_RESULTS) {
+ this.phase = this.phases.HAVE_RESULTS;
+ }
+ this.num_pending--;
+ this.notify_result(test);
+ };
+
+ Tests.prototype.notify_result = function(test) {
+ var this_obj = this;
+ this.processing_callbacks = true;
+ forEach(this.test_done_callbacks,
+ function(callback)
+ {
+ callback(test, this_obj);
+ });
+ this.processing_callbacks = false;
+ if (this_obj.all_done()) {
+ this_obj.complete();
+ }
+ };
+
+ Tests.prototype.complete = function() {
+ if (this.phase === this.phases.COMPLETE) {
+ return;
+ }
+ var this_obj = this;
+ var all_complete = function() {
+ this_obj.phase = this_obj.phases.COMPLETE;
+ this_obj.notify_complete();
+ };
+ var incomplete = filter(this.tests,
+ function(test) {
+ return test.phase < test.phases.COMPLETE;
+ });
+
+ /**
+ * To preserve legacy behavior, overall test completion must be
+ * signaled synchronously.
+ */
+ if (incomplete.length === 0) {
+ all_complete();
+ return;
+ }
+
+ all_async(incomplete,
+ function(test, testDone)
+ {
+ if (test.phase === test.phases.INITIAL) {
+ test.phase = test.phases.COMPLETE;
+ testDone();
+ } else {
+ add_test_done_callback(test, testDone);
+ test.cleanup();
+ }
+ },
+ all_complete);
+ };
+
+ Tests.prototype.set_assert = function(assert_name, args) {
+ this.asserts_run.push(new AssertRecord(this.current_test, assert_name, args))
+ }
+
+ Tests.prototype.set_assert_status = function(index, status, stack) {
+ let assert_record = this.asserts_run[index];
+ assert_record.status = status;
+ assert_record.stack = stack;
+ }
+
+ /**
+ * Update the harness status to reflect an unrecoverable harness error that
+ * should cancel all further testing. Update all previously-defined tests
+ * which have not yet started to indicate that they will not be executed.
+ */
+ Tests.prototype.abort = function() {
+ this.status.status = this.status.ERROR;
+ this.is_aborted = true;
+
+ forEach(this.tests,
+ function(test) {
+ if (test.phase === test.phases.INITIAL) {
+ test.phase = test.phases.COMPLETE;
+ }
+ });
+ };
+
+ /*
+ * Determine if any tests share the same `name` property. Return an array
+ * containing the names of any such duplicates.
+ */
+ Tests.prototype.find_duplicates = function() {
+ var names = Object.create(null);
+ var duplicates = [];
+
+ forEach (this.tests,
+ function(test)
+ {
+ if (test.name in names && duplicates.indexOf(test.name) === -1) {
+ duplicates.push(test.name);
+ }
+ names[test.name] = true;
+ });
+
+ return duplicates;
+ };
+
+ function code_unit_str(char) {
+ return 'U+' + char.charCodeAt(0).toString(16);
+ }
+
+ function sanitize_unpaired_surrogates(str) {
+ return str.replace(
+ /([\ud800-\udbff]+)(?![\udc00-\udfff])|(^|[^\ud800-\udbff])([\udc00-\udfff]+)/g,
+ function(_, low, prefix, high) {
+ var output = prefix || ""; // prefix may be undefined
+ var string = low || high; // only one of these alternates can match
+ for (var i = 0; i < string.length; i++) {
+ output += code_unit_str(string[i]);
+ }
+ return output;
+ });
+ }
+
+ function sanitize_all_unpaired_surrogates(tests) {
+ forEach (tests,
+ function (test)
+ {
+ var sanitized = sanitize_unpaired_surrogates(test.name);
+
+ if (test.name !== sanitized) {
+ test.name = sanitized;
+ delete test._structured_clone;
+ }
+ });
+ }
+
+ Tests.prototype.notify_complete = function() {
+ var this_obj = this;
+ var duplicates;
+
+ if (this.status.status === null) {
+ duplicates = this.find_duplicates();
+
+ // Some transports adhere to UTF-8's restriction on unpaired
+ // surrogates. Sanitize the titles so that the results can be
+ // consistently sent via all transports.
+ sanitize_all_unpaired_surrogates(this.tests);
+
+ // Test names are presumed to be unique within test files--this
+ // allows consumers to use them for identification purposes.
+ // Duplicated names violate this expectation and should therefore
+ // be reported as an error.
+ if (duplicates.length) {
+ this.status.status = this.status.ERROR;
+ this.status.message =
+ duplicates.length + ' duplicate test name' +
+ (duplicates.length > 1 ? 's' : '') + ': "' +
+ duplicates.join('", "') + '"';
+ } else {
+ this.status.status = this.status.OK;
+ }
+ }
+
+ forEach (this.all_done_callbacks,
+ function(callback)
+ {
+ callback(this_obj.tests, this_obj.status, this_obj.asserts_run);
+ });
+ };
+
+ /*
+ * Constructs a RemoteContext that tracks tests from a specific worker.
+ */
+ Tests.prototype.create_remote_worker = function(worker) {
+ var message_port;
+
+ if (is_service_worker(worker)) {
+ message_port = navigator.serviceWorker;
+ worker.postMessage({type: "connect"});
+ } else if (is_shared_worker(worker)) {
+ message_port = worker.port;
+ message_port.start();
+ } else {
+ message_port = worker;
+ }
+
+ return new RemoteContext(worker, message_port);
+ };
+
+ /*
+ * Constructs a RemoteContext that tracks tests from a specific window.
+ */
+ Tests.prototype.create_remote_window = function(remote) {
+ remote.postMessage({type: "getmessages"}, "*");
+ return new RemoteContext(
+ remote,
+ window,
+ function(msg) {
+ return msg.source === remote;
+ }
+ );
+ };
+
+ Tests.prototype.fetch_tests_from_worker = function(worker) {
+ if (this.phase >= this.phases.COMPLETE) {
+ return;
+ }
+
+ var remoteContext = this.create_remote_worker(worker);
+ this.pending_remotes.push(remoteContext);
+ return remoteContext.done;
+ };
+
+ /**
+ * Get test results from a worker and include them in the current test.
+ *
+ * @param {Worker|SharedWorker|ServiceWorker|MessagePort} port -
+ * Either a worker object or a port connected to a worker which is
+ * running tests..
+ * @returns {Promise} - A promise that's resolved once all the remote tests are complete.
+ */
+ function fetch_tests_from_worker(port) {
+ return tests.fetch_tests_from_worker(port);
+ }
+ expose(fetch_tests_from_worker, 'fetch_tests_from_worker');
+
+ Tests.prototype.fetch_tests_from_window = function(remote) {
+ if (this.phase >= this.phases.COMPLETE) {
+ return;
+ }
+
+ var remoteContext = this.create_remote_window(remote);
+ this.pending_remotes.push(remoteContext);
+ return remoteContext.done;
+ };
+
+ /**
+ * Aggregate tests from separate windows or iframes
+ * into the current document as if they were all part of the same test file.
+ *
+ * The document of the second window (or iframe) should include
+ * ``testharness.js``, but not ``testharnessreport.js``, and use
+ * :js:func:`test`, :js:func:`async_test`, and :js:func:`promise_test` in
+ * the usual manner.
+ *
+ * @param {Window} window - The window to fetch tests from.
+ */
+ function fetch_tests_from_window(window) {
+ return tests.fetch_tests_from_window(window);
+ }
+ expose(fetch_tests_from_window, 'fetch_tests_from_window');
+
+ /**
+ * Get test results from a shadow realm and include them in the current test.
+ *
+ * @param {ShadowRealm} realm - A shadow realm also running the test harness
+ * @returns {Promise} - A promise that's resolved once all the remote tests are complete.
+ */
+ function fetch_tests_from_shadow_realm(realm) {
+ var chan = new MessageChannel();
+ function receiveMessage(msg_json) {
+ chan.port1.postMessage(JSON.parse(msg_json));
+ }
+ var done = tests.fetch_tests_from_worker(chan.port2);
+ realm.evaluate("begin_shadow_realm_tests")(receiveMessage);
+ chan.port2.start();
+ return done;
+ }
+ expose(fetch_tests_from_shadow_realm, 'fetch_tests_from_shadow_realm');
+
+ /**
+ * Begin running tests in this shadow realm test harness.
+ *
+ * To be called after all tests have been loaded; it is an error to call
+ * this more than once or in a non-Shadow Realm environment
+ *
+ * @param {Function} postMessage - A function to send test updates to the
+ * incubating realm-- accepts JSON-encoded messages in the format used by
+ * RemoteContext
+ */
+ function begin_shadow_realm_tests(postMessage) {
+ if (!(test_environment instanceof ShadowRealmTestEnvironment)) {
+ throw new Error("begin_shadow_realm_tests called in non-Shadow Realm environment");
+ }
+
+ test_environment.begin(function (msg) {
+ postMessage(JSON.stringify(msg));
+ });
+ }
+ expose(begin_shadow_realm_tests, 'begin_shadow_realm_tests');
+
+ /**
+ * Timeout the tests.
+ *
+ * This only has an effect when ``explicit_timeout`` has been set
+ * in :js:func:`setup`. In other cases any call is a no-op.
+ *
+ */
+ function timeout() {
+ if (tests.timeout_length === null) {
+ tests.timeout();
+ }
+ }
+ expose(timeout, 'timeout');
+
+ /**
+ * Add a callback that's triggered when the first :js:class:`Test` is created.
+ *
+ * @param {Function} callback - Callback function. This is called
+ * without arguments.
+ */
+ function add_start_callback(callback) {
+ tests.start_callbacks.push(callback);
+ }
+
+ /**
+ * Add a callback that's triggered when a test state changes.
+ *
+ * @param {Function} callback - Callback function, called with the
+ * :js:class:`Test` as the only argument.
+ */
+ function add_test_state_callback(callback) {
+ tests.test_state_callbacks.push(callback);
+ }
+
+ /**
+ * Add a callback that's triggered when a test result is received.
+ *
+ * @param {Function} callback - Callback function, called with the
+ * :js:class:`Test` as the only argument.
+ */
+ function add_result_callback(callback) {
+ tests.test_done_callbacks.push(callback);
+ }
+
+ /**
+ * Add a callback that's triggered when all tests are complete.
+ *
+ * @param {Function} callback - Callback function, called with an
+ * array of :js:class:`Test` objects, a :js:class:`TestsStatus`
+ * object and an array of :js:class:`AssertRecord` objects. If the
+ * debug setting is ``false`` the final argument will be an empty
+ * array.
+ *
+ * For performance reasons asserts are only tracked when the debug
+ * setting is ``true``. In other cases the array of asserts will be
+ * empty.
+ */
+ function add_completion_callback(callback) {
+ tests.all_done_callbacks.push(callback);
+ }
+
+ expose(add_start_callback, 'add_start_callback');
+ expose(add_test_state_callback, 'add_test_state_callback');
+ expose(add_result_callback, 'add_result_callback');
+ expose(add_completion_callback, 'add_completion_callback');
+
+ function remove(array, item) {
+ var index = array.indexOf(item);
+ if (index > -1) {
+ array.splice(index, 1);
+ }
+ }
+
+ function remove_start_callback(callback) {
+ remove(tests.start_callbacks, callback);
+ }
+
+ function remove_test_state_callback(callback) {
+ remove(tests.test_state_callbacks, callback);
+ }
+
+ function remove_result_callback(callback) {
+ remove(tests.test_done_callbacks, callback);
+ }
+
+ function remove_completion_callback(callback) {
+ remove(tests.all_done_callbacks, callback);
+ }
+
+ /*
+ * Output listener
+ */
+
+ function Output() {
+ this.output_document = document;
+ this.output_node = null;
+ this.enabled = settings.output;
+ this.phase = this.INITIAL;
+ }
+
+ Output.prototype.INITIAL = 0;
+ Output.prototype.STARTED = 1;
+ Output.prototype.HAVE_RESULTS = 2;
+ Output.prototype.COMPLETE = 3;
+
+ Output.prototype.setup = function(properties) {
+ if (this.phase > this.INITIAL) {
+ return;
+ }
+
+ //If output is disabled in testharnessreport.js the test shouldn't be
+ //able to override that
+ this.enabled = this.enabled && (properties.hasOwnProperty("output") ?
+ properties.output : settings.output);
+ };
+
+ Output.prototype.init = function(properties) {
+ if (this.phase >= this.STARTED) {
+ return;
+ }
+ if (properties.output_document) {
+ this.output_document = properties.output_document;
+ } else {
+ this.output_document = document;
+ }
+ this.phase = this.STARTED;
+ };
+
+ Output.prototype.resolve_log = function() {
+ var output_document;
+ if (this.output_node) {
+ return;
+ }
+ if (typeof this.output_document === "function") {
+ output_document = this.output_document.apply(undefined);
+ } else {
+ output_document = this.output_document;
+ }
+ if (!output_document) {
+ return;
+ }
+ var node = output_document.getElementById("log");
+ if (!node) {
+ if (output_document.readyState === "loading") {
+ return;
+ }
+ node = output_document.createElementNS("http://www.w3.org/1999/xhtml", "div");
+ node.id = "log";
+ if (output_document.body) {
+ output_document.body.appendChild(node);
+ } else {
+ var root = output_document.documentElement;
+ var is_html = (root &&
+ root.namespaceURI == "http://www.w3.org/1999/xhtml" &&
+ root.localName == "html");
+ var is_svg = (output_document.defaultView &&
+ "SVGSVGElement" in output_document.defaultView &&
+ root instanceof output_document.defaultView.SVGSVGElement);
+ if (is_svg) {
+ var foreignObject = output_document.createElementNS("http://www.w3.org/2000/svg", "foreignObject");
+ foreignObject.setAttribute("width", "100%");
+ foreignObject.setAttribute("height", "100%");
+ root.appendChild(foreignObject);
+ foreignObject.appendChild(node);
+ } else if (is_html) {
+ root.appendChild(output_document.createElementNS("http://www.w3.org/1999/xhtml", "body"))
+ .appendChild(node);
+ } else {
+ root.appendChild(node);
+ }
+ }
+ }
+ this.output_document = output_document;
+ this.output_node = node;
+ };
+
+ Output.prototype.show_status = function() {
+ if (this.phase < this.STARTED) {
+ this.init({});
+ }
+ if (!this.enabled || this.phase === this.COMPLETE) {
+ return;
+ }
+ this.resolve_log();
+ if (this.phase < this.HAVE_RESULTS) {
+ this.phase = this.HAVE_RESULTS;
+ }
+ var done_count = tests.tests.length - tests.num_pending;
+ if (this.output_node && !tests.hide_test_state) {
+ if (done_count < 100 ||
+ (done_count < 1000 && done_count % 100 === 0) ||
+ done_count % 1000 === 0) {
+ this.output_node.textContent = "Running, " +
+ done_count + " complete, " +
+ tests.num_pending + " remain";
+ }
+ }
+ };
+
+ Output.prototype.show_results = function (tests, harness_status, asserts_run) {
+ if (this.phase >= this.COMPLETE) {
+ return;
+ }
+ if (!this.enabled) {
+ return;
+ }
+ if (!this.output_node) {
+ this.resolve_log();
+ }
+ this.phase = this.COMPLETE;
+
+ var log = this.output_node;
+ if (!log) {
+ return;
+ }
+ var output_document = this.output_document;
+
+ while (log.lastChild) {
+ log.removeChild(log.lastChild);
+ }
+
+ var stylesheet = output_document.createElementNS(xhtml_ns, "style");
+ stylesheet.textContent = stylesheetContent;
+ var heads = output_document.getElementsByTagName("head");
+ if (heads.length) {
+ heads[0].appendChild(stylesheet);
+ }
+
+ var status_number = {};
+ forEach(tests,
+ function(test) {
+ var status = test.format_status();
+ if (status_number.hasOwnProperty(status)) {
+ status_number[status] += 1;
+ } else {
+ status_number[status] = 1;
+ }
+ });
+
+ function status_class(status)
+ {
+ return status.replace(/\s/g, '').toLowerCase();
+ }
+
+ var summary_template = ["section", {"id":"summary"},
+ ["h2", {}, "Summary"],
+ function()
+ {
+ var status = harness_status.format_status();
+ var rv = [["section", {},
+ ["p", {},
+ "Harness status: ",
+ ["span", {"class":status_class(status)},
+ status
+ ],
+ ],
+ ["button",
+ {"onclick": "let evt = new Event('__test_restart'); " +
+ "let canceled = !window.dispatchEvent(evt);" +
+ "if (!canceled) { location.reload() }"},
+ "Rerun"]
+ ]];
+
+ if (harness_status.status === harness_status.ERROR) {
+ rv[0].push(["pre", {}, harness_status.message]);
+ if (harness_status.stack) {
+ rv[0].push(["pre", {}, harness_status.stack]);
+ }
+ }
+ return rv;
+ },
+ ["p", {}, "Found ${num_tests} tests"],
+ function() {
+ var rv = [["div", {}]];
+ var i = 0;
+ while (Test.prototype.status_formats.hasOwnProperty(i)) {
+ if (status_number.hasOwnProperty(Test.prototype.status_formats[i])) {
+ var status = Test.prototype.status_formats[i];
+ rv[0].push(["div", {},
+ ["label", {},
+ ["input", {type:"checkbox", checked:"checked"}],
+ status_number[status] + " ",
+ ["span", {"class":status_class(status)}, status]]]);
+ }
+ i++;
+ }
+ return rv;
+ },
+ ];
+
+ log.appendChild(render(summary_template, {num_tests:tests.length}, output_document));
+
+ forEach(output_document.querySelectorAll("section#summary label"),
+ function(element)
+ {
+ on_event(element, "click",
+ function(e)
+ {
+ if (output_document.getElementById("results") === null) {
+ e.preventDefault();
+ return;
+ }
+ var result_class = element.querySelector("span[class]").getAttribute("class");
+ var style_element = output_document.querySelector("style#hide-" + result_class);
+ var input_element = element.querySelector("input");
+ if (!style_element && !input_element.checked) {
+ style_element = output_document.createElementNS(xhtml_ns, "style");
+ style_element.id = "hide-" + result_class;
+ style_element.textContent = "table#results > tbody > tr.overall-"+result_class+"{display:none}";
+ output_document.body.appendChild(style_element);
+ } else if (style_element && input_element.checked) {
+ style_element.parentNode.removeChild(style_element);
+ }
+ });
+ });
+
+ // This use of innerHTML plus manual escaping is not recommended in
+ // general, but is necessary here for performance. Using textContent
+ // on each individual <td> adds tens of seconds of execution time for
+ // large test suites (tens of thousands of tests).
+ function escape_html(s)
+ {
+ return s.replace(/\&/g, "&amp;")
+ .replace(/</g, "&lt;")
+ .replace(/"/g, "&quot;")
+ .replace(/'/g, "&#39;");
+ }
+
+ function has_assertions()
+ {
+ for (var i = 0; i < tests.length; i++) {
+ if (tests[i].properties.hasOwnProperty("assert")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ function get_assertion(test)
+ {
+ if (test.properties.hasOwnProperty("assert")) {
+ if (Array.isArray(test.properties.assert)) {
+ return test.properties.assert.join(' ');
+ }
+ return test.properties.assert;
+ }
+ return '';
+ }
+
+ var asserts_run_by_test = new Map();
+ asserts_run.forEach(assert => {
+ if (!asserts_run_by_test.has(assert.test)) {
+ asserts_run_by_test.set(assert.test, []);
+ }
+ asserts_run_by_test.get(assert.test).push(assert);
+ });
+
+ function get_asserts_output(test) {
+ var asserts = asserts_run_by_test.get(test);
+ if (!asserts) {
+ return "No asserts ran";
+ }
+ rv = "<table>";
+ rv += asserts.map(assert => {
+ var output_fn = "<strong>" + escape_html(assert.assert_name) + "</strong>(";
+ var prefix_len = output_fn.length;
+ var output_args = assert.args;
+ var output_len = output_args.reduce((prev, current) => prev+current, prefix_len);
+ if (output_len[output_len.length - 1] > 50) {
+ output_args = output_args.map((x, i) =>
+ (i > 0 ? " ".repeat(prefix_len) : "" )+ x + (i < output_args.length - 1 ? ",\n" : ""));
+ } else {
+ output_args = output_args.map((x, i) => x + (i < output_args.length - 1 ? ", " : ""));
+ }
+ output_fn += escape_html(output_args.join(""));
+ output_fn += ')';
+ var output_location;
+ if (assert.stack) {
+ output_location = assert.stack.split("\n", 1)[0].replace(/@?\w+:\/\/[^ "\/]+(?::\d+)?/g, " ");
+ }
+ return "<tr class='overall-" +
+ status_class(Test.prototype.status_formats[assert.status]) + "'>" +
+ "<td class='" +
+ status_class(Test.prototype.status_formats[assert.status]) + "'>" +
+ Test.prototype.status_formats[assert.status] + "</td>" +
+ "<td><pre>" +
+ output_fn +
+ (output_location ? "\n" + escape_html(output_location) : "") +
+ "</pre></td></tr>";
+ }
+ ).join("\n");
+ rv += "</table>";
+ return rv;
+ }
+
+ log.appendChild(document.createElementNS(xhtml_ns, "section"));
+ var assertions = has_assertions();
+ var html = "<h2>Details</h2><table id='results' " + (assertions ? "class='assertions'" : "" ) + ">" +
+ "<thead><tr><th>Result</th><th>Test Name</th>" +
+ (assertions ? "<th>Assertion</th>" : "") +
+ "<th>Message</th></tr></thead>" +
+ "<tbody>";
+ for (var i = 0; i < tests.length; i++) {
+ var test = tests[i];
+ html += '<tr class="overall-' +
+ status_class(test.format_status()) +
+ '">' +
+ '<td class="' +
+ status_class(test.format_status()) +
+ '">' +
+ test.format_status() +
+ "</td><td>" +
+ escape_html(test.name) +
+ "</td><td>" +
+ (assertions ? escape_html(get_assertion(test)) + "</td><td>" : "") +
+ escape_html(test.message ? tests[i].message : " ") +
+ (tests[i].stack ? "<pre>" +
+ escape_html(tests[i].stack) +
+ "</pre>": "");
+ if (!(test instanceof RemoteTest)) {
+ html += "<details><summary>Asserts run</summary>" + get_asserts_output(test) + "</details>"
+ }
+ html += "</td></tr>";
+ }
+ html += "</tbody></table>";
+ try {
+ log.lastChild.innerHTML = html;
+ } catch (e) {
+ log.appendChild(document.createElementNS(xhtml_ns, "p"))
+ .textContent = "Setting innerHTML for the log threw an exception.";
+ log.appendChild(document.createElementNS(xhtml_ns, "pre"))
+ .textContent = html;
+ }
+ };
+
+ /*
+ * Template code
+ *
+ * A template is just a JavaScript structure. An element is represented as:
+ *
+ * [tag_name, {attr_name:attr_value}, child1, child2]
+ *
+ * the children can either be strings (which act like text nodes), other templates or
+ * functions (see below)
+ *
+ * A text node is represented as
+ *
+ * ["{text}", value]
+ *
+ * String values have a simple substitution syntax; ${foo} represents a variable foo.
+ *
+ * It is possible to embed logic in templates by using a function in a place where a
+ * node would usually go. The function must either return part of a template or null.
+ *
+ * In cases where a set of nodes are required as output rather than a single node
+ * with children it is possible to just use a list
+ * [node1, node2, node3]
+ *
+ * Usage:
+ *
+ * render(template, substitutions) - take a template and an object mapping
+ * variable names to parameters and return either a DOM node or a list of DOM nodes
+ *
+ * substitute(template, substitutions) - take a template and variable mapping object,
+ * make the variable substitutions and return the substituted template
+ *
+ */
+
+ function is_single_node(template)
+ {
+ return typeof template[0] === "string";
+ }
+
+ function substitute(template, substitutions)
+ {
+ if (typeof template === "function") {
+ var replacement = template(substitutions);
+ if (!replacement) {
+ return null;
+ }
+
+ return substitute(replacement, substitutions);
+ }
+
+ if (is_single_node(template)) {
+ return substitute_single(template, substitutions);
+ }
+
+ return filter(map(template, function(x) {
+ return substitute(x, substitutions);
+ }), function(x) {return x !== null;});
+ }
+
+ function substitute_single(template, substitutions)
+ {
+ var substitution_re = /\$\{([^ }]*)\}/g;
+
+ function do_substitution(input) {
+ var components = input.split(substitution_re);
+ var rv = [];
+ for (var i = 0; i < components.length; i += 2) {
+ rv.push(components[i]);
+ if (components[i + 1]) {
+ rv.push(String(substitutions[components[i + 1]]));
+ }
+ }
+ return rv;
+ }
+
+ function substitute_attrs(attrs, rv)
+ {
+ rv[1] = {};
+ for (var name in template[1]) {
+ if (attrs.hasOwnProperty(name)) {
+ var new_name = do_substitution(name).join("");
+ var new_value = do_substitution(attrs[name]).join("");
+ rv[1][new_name] = new_value;
+ }
+ }
+ }
+
+ function substitute_children(children, rv)
+ {
+ for (var i = 0; i < children.length; i++) {
+ if (children[i] instanceof Object) {
+ var replacement = substitute(children[i], substitutions);
+ if (replacement !== null) {
+ if (is_single_node(replacement)) {
+ rv.push(replacement);
+ } else {
+ extend(rv, replacement);
+ }
+ }
+ } else {
+ extend(rv, do_substitution(String(children[i])));
+ }
+ }
+ return rv;
+ }
+
+ var rv = [];
+ rv.push(do_substitution(String(template[0])).join(""));
+
+ if (template[0] === "{text}") {
+ substitute_children(template.slice(1), rv);
+ } else {
+ substitute_attrs(template[1], rv);
+ substitute_children(template.slice(2), rv);
+ }
+
+ return rv;
+ }
+
+ function make_dom_single(template, doc)
+ {
+ var output_document = doc || document;
+ var element;
+ if (template[0] === "{text}") {
+ element = output_document.createTextNode("");
+ for (var i = 1; i < template.length; i++) {
+ element.data += template[i];
+ }
+ } else {
+ element = output_document.createElementNS(xhtml_ns, template[0]);
+ for (var name in template[1]) {
+ if (template[1].hasOwnProperty(name)) {
+ element.setAttribute(name, template[1][name]);
+ }
+ }
+ for (var i = 2; i < template.length; i++) {
+ if (template[i] instanceof Object) {
+ var sub_element = make_dom(template[i]);
+ element.appendChild(sub_element);
+ } else {
+ var text_node = output_document.createTextNode(template[i]);
+ element.appendChild(text_node);
+ }
+ }
+ }
+
+ return element;
+ }
+
+ function make_dom(template, substitutions, output_document)
+ {
+ if (is_single_node(template)) {
+ return make_dom_single(template, output_document);
+ }
+
+ return map(template, function(x) {
+ return make_dom_single(x, output_document);
+ });
+ }
+
+ function render(template, substitutions, output_document)
+ {
+ return make_dom(substitute(template, substitutions), output_document);
+ }
+
+ /*
+ * Utility functions
+ */
+ function assert(expected_true, function_name, description, error, substitutions)
+ {
+ if (expected_true !== true) {
+ var msg = make_message(function_name, description,
+ error, substitutions);
+ throw new AssertionError(msg);
+ }
+ }
+
+ /**
+ * @class
+ * Exception type that represents a failing assert.
+ *
+ * @param {string} message - Error message.
+ */
+ function AssertionError(message)
+ {
+ if (typeof message == "string") {
+ message = sanitize_unpaired_surrogates(message);
+ }
+ this.message = message;
+ this.stack = get_stack();
+ }
+ expose(AssertionError, "AssertionError");
+
+ AssertionError.prototype = Object.create(Error.prototype);
+
+ const get_stack = function() {
+ var stack = new Error().stack;
+
+ // 'Error.stack' is not supported in all browsers/versions
+ if (!stack) {
+ return "(Stack trace unavailable)";
+ }
+
+ var lines = stack.split("\n");
+
+ // Create a pattern to match stack frames originating within testharness.js. These include the
+ // script URL, followed by the line/col (e.g., '/resources/testharness.js:120:21').
+ // Escape the URL per http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
+ // in case it contains RegExp characters.
+ var script_url = get_script_url();
+ var re_text = script_url ? script_url.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') : "\\btestharness.js";
+ var re = new RegExp(re_text + ":\\d+:\\d+");
+
+ // Some browsers include a preamble that specifies the type of the error object. Skip this by
+ // advancing until we find the first stack frame originating from testharness.js.
+ var i = 0;
+ while (!re.test(lines[i]) && i < lines.length) {
+ i++;
+ }
+
+ // Then skip the top frames originating from testharness.js to begin the stack at the test code.
+ while (re.test(lines[i]) && i < lines.length) {
+ i++;
+ }
+
+ // Paranoid check that we didn't skip all frames. If so, return the original stack unmodified.
+ if (i >= lines.length) {
+ return stack;
+ }
+
+ return lines.slice(i).join("\n");
+ }
+
+ function OptionalFeatureUnsupportedError(message)
+ {
+ AssertionError.call(this, message);
+ }
+ OptionalFeatureUnsupportedError.prototype = Object.create(AssertionError.prototype);
+ expose(OptionalFeatureUnsupportedError, "OptionalFeatureUnsupportedError");
+
+ function make_message(function_name, description, error, substitutions)
+ {
+ for (var p in substitutions) {
+ if (substitutions.hasOwnProperty(p)) {
+ substitutions[p] = format_value(substitutions[p]);
+ }
+ }
+ var node_form = substitute(["{text}", "${function_name}: ${description}" + error],
+ merge({function_name:function_name,
+ description:(description?description + " ":"")},
+ substitutions));
+ return node_form.slice(1).join("");
+ }
+
+ function filter(array, callable, thisObj) {
+ var rv = [];
+ for (var i = 0; i < array.length; i++) {
+ if (array.hasOwnProperty(i)) {
+ var pass = callable.call(thisObj, array[i], i, array);
+ if (pass) {
+ rv.push(array[i]);
+ }
+ }
+ }
+ return rv;
+ }
+
+ function map(array, callable, thisObj)
+ {
+ var rv = [];
+ rv.length = array.length;
+ for (var i = 0; i < array.length; i++) {
+ if (array.hasOwnProperty(i)) {
+ rv[i] = callable.call(thisObj, array[i], i, array);
+ }
+ }
+ return rv;
+ }
+
+ function extend(array, items)
+ {
+ Array.prototype.push.apply(array, items);
+ }
+
+ function forEach(array, callback, thisObj)
+ {
+ for (var i = 0; i < array.length; i++) {
+ if (array.hasOwnProperty(i)) {
+ callback.call(thisObj, array[i], i, array);
+ }
+ }
+ }
+
+ /**
+ * Immediately invoke a "iteratee" function with a series of values in
+ * parallel and invoke a final "done" function when all of the "iteratee"
+ * invocations have signaled completion.
+ *
+ * If all callbacks complete synchronously (or if no callbacks are
+ * specified), the ``done_callback`` will be invoked synchronously. It is the
+ * responsibility of the caller to ensure asynchronicity in cases where
+ * that is desired.
+ *
+ * @param {array} value Zero or more values to use in the invocation of
+ * ``iter_callback``
+ * @param {function} iter_callback A function that will be invoked
+ * once for each of the values min
+ * ``value``. Two arguments will
+ * be available in each
+ * invocation: the value from
+ * ``value`` and a function that
+ * must be invoked to signal
+ * completion
+ * @param {function} done_callback A function that will be invoked after
+ * all operations initiated by the
+ * ``iter_callback`` function have signaled
+ * completion
+ */
+ function all_async(values, iter_callback, done_callback)
+ {
+ var remaining = values.length;
+
+ if (remaining === 0) {
+ done_callback();
+ }
+
+ forEach(values,
+ function(element) {
+ var invoked = false;
+ var elDone = function() {
+ if (invoked) {
+ return;
+ }
+
+ invoked = true;
+ remaining -= 1;
+
+ if (remaining === 0) {
+ done_callback();
+ }
+ };
+
+ iter_callback(element, elDone);
+ });
+ }
+
+ function merge(a,b)
+ {
+ var rv = {};
+ var p;
+ for (p in a) {
+ rv[p] = a[p];
+ }
+ for (p in b) {
+ rv[p] = b[p];
+ }
+ return rv;
+ }
+
+ function expose(object, name)
+ {
+ var components = name.split(".");
+ var target = global_scope;
+ for (var i = 0; i < components.length - 1; i++) {
+ if (!(components[i] in target)) {
+ target[components[i]] = {};
+ }
+ target = target[components[i]];
+ }
+ target[components[components.length - 1]] = object;
+ }
+
+ function is_same_origin(w) {
+ try {
+ 'random_prop' in w;
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ /** Returns the 'src' URL of the first <script> tag in the page to include the file 'testharness.js'. */
+ function get_script_url()
+ {
+ if (!('document' in global_scope)) {
+ return undefined;
+ }
+
+ var scripts = document.getElementsByTagName("script");
+ for (var i = 0; i < scripts.length; i++) {
+ var src;
+ if (scripts[i].src) {
+ src = scripts[i].src;
+ } else if (scripts[i].href) {
+ //SVG case
+ src = scripts[i].href.baseVal;
+ }
+
+ var matches = src && src.match(/^(.*\/|)testharness\.js$/);
+ if (matches) {
+ return src;
+ }
+ }
+ return undefined;
+ }
+
+ /** Returns the <title> or filename or "Untitled" */
+ function get_title()
+ {
+ if ('document' in global_scope) {
+ //Don't use document.title to work around an Opera/Presto bug in XHTML documents
+ var title = document.getElementsByTagName("title")[0];
+ if (title && title.firstChild && title.firstChild.data) {
+ return title.firstChild.data;
+ }
+ }
+ if ('META_TITLE' in global_scope && META_TITLE) {
+ return META_TITLE;
+ }
+ if ('location' in global_scope && 'pathname' in location) {
+ return location.pathname.substring(location.pathname.lastIndexOf('/') + 1, location.pathname.indexOf('.'));
+ }
+ return "Untitled";
+ }
+
+ /**
+ * Setup globals
+ */
+
+ var tests = new Tests();
+
+ if (global_scope.addEventListener) {
+ var error_handler = function(error, message, stack) {
+ var optional_unsupported = error instanceof OptionalFeatureUnsupportedError;
+ if (tests.file_is_test) {
+ var test = tests.tests[0];
+ if (test.phase >= test.phases.HAS_RESULT) {
+ return;
+ }
+ var status = optional_unsupported ? test.PRECONDITION_FAILED : test.FAIL;
+ test.set_status(status, message, stack);
+ test.phase = test.phases.HAS_RESULT;
+ } else if (!tests.allow_uncaught_exception) {
+ var status = optional_unsupported ? tests.status.PRECONDITION_FAILED : tests.status.ERROR;
+ tests.status.status = status;
+ tests.status.message = message;
+ tests.status.stack = stack;
+ }
+
+ // Do not transition to the "complete" phase if the test has been
+ // configured to allow uncaught exceptions. This gives the test an
+ // opportunity to define subtests based on the exception reporting
+ // behavior.
+ if (!tests.allow_uncaught_exception) {
+ done();
+ }
+ };
+
+ addEventListener("error", function(e) {
+ var message = e.message;
+ var stack;
+ if (e.error && e.error.stack) {
+ stack = e.error.stack;
+ } else {
+ stack = e.filename + ":" + e.lineno + ":" + e.colno;
+ }
+ error_handler(e.error, message, stack);
+ }, false);
+
+ addEventListener("unhandledrejection", function(e) {
+ var message;
+ if (e.reason && e.reason.message) {
+ message = "Unhandled rejection: " + e.reason.message;
+ } else {
+ message = "Unhandled rejection";
+ }
+ var stack;
+ if (e.reason && e.reason.stack) {
+ stack = e.reason.stack;
+ }
+ error_handler(e.reason, message, stack);
+ }, false);
+ }
+
+ test_environment.on_tests_ready();
+
+ /**
+ * Stylesheet
+ */
+ var stylesheetContent = "\
+html {\
+ font-family:DejaVu Sans, Bitstream Vera Sans, Arial, Sans;\
+}\
+\
+#log .warning,\
+#log .warning a {\
+ color: black;\
+ background: yellow;\
+}\
+\
+#log .error,\
+#log .error a {\
+ color: white;\
+ background: red;\
+}\
+\
+section#summary {\
+ margin-bottom:1em;\
+}\
+\
+table#results {\
+ border-collapse:collapse;\
+ table-layout:fixed;\
+ width:100%;\
+}\
+\
+table#results > thead > tr > th:first-child,\
+table#results > tbody > tr > td:first-child {\
+ width:8em;\
+}\
+\
+table#results > thead > tr > th:last-child,\
+table#results > thead > tr > td:last-child {\
+ width:50%;\
+}\
+\
+table#results.assertions > thead > tr > th:last-child,\
+table#results.assertions > tbody > tr > td:last-child {\
+ width:35%;\
+}\
+\
+table#results > thead > > tr > th {\
+ padding:0;\
+ padding-bottom:0.5em;\
+ border-bottom:medium solid black;\
+}\
+\
+table#results > tbody > tr> td {\
+ padding:1em;\
+ padding-bottom:0.5em;\
+ border-bottom:thin solid black;\
+}\
+\
+.pass {\
+ color:green;\
+}\
+\
+.fail {\
+ color:red;\
+}\
+\
+tr.timeout {\
+ color:red;\
+}\
+\
+tr.notrun {\
+ color:blue;\
+}\
+\
+tr.optionalunsupported {\
+ color:blue;\
+}\
+\
+.ok {\
+ color:green;\
+}\
+\
+.error {\
+ color:red;\
+}\
+\
+.pass, .fail, .timeout, .notrun, .optionalunsupported .ok, .timeout, .error {\
+ font-variant:small-caps;\
+}\
+\
+table#results span {\
+ display:block;\
+}\
+\
+table#results span.expected {\
+ font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace;\
+ white-space:pre;\
+}\
+\
+table#results span.actual {\
+ font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace;\
+ white-space:pre;\
+}\
+";
+
+})(self);
+// vim: set expandtab shiftwidth=4 tabstop=4:
diff --git a/testing/web-platform/tests/resources/testharness.js.headers b/testing/web-platform/tests/resources/testharness.js.headers
new file mode 100644
index 0000000000..5e8f640c66
--- /dev/null
+++ b/testing/web-platform/tests/resources/testharness.js.headers
@@ -0,0 +1,2 @@
+Content-Type: text/javascript; charset=utf-8
+Cache-Control: max-age=3600
diff --git a/testing/web-platform/tests/resources/testharnessreport.js b/testing/web-platform/tests/resources/testharnessreport.js
new file mode 100644
index 0000000000..e5cb40fe0e
--- /dev/null
+++ b/testing/web-platform/tests/resources/testharnessreport.js
@@ -0,0 +1,57 @@
+/* global add_completion_callback */
+/* global setup */
+
+/*
+ * This file is intended for vendors to implement code needed to integrate
+ * testharness.js tests with their own test systems.
+ *
+ * Typically test system integration will attach callbacks when each test has
+ * run, using add_result_callback(callback(test)), or when the whole test file
+ * has completed, using
+ * add_completion_callback(callback(tests, harness_status)).
+ *
+ * For more documentation about the callback functions and the
+ * parameters they are called with see testharness.js
+ */
+
+function dump_test_results(tests, status) {
+ var results_element = document.createElement("script");
+ results_element.type = "text/json";
+ results_element.id = "__testharness__results__";
+ var test_results = tests.map(function(x) {
+ return {name:x.name, status:x.status, message:x.message, stack:x.stack}
+ });
+ var data = {test:window.location.href,
+ tests:test_results,
+ status: status.status,
+ message: status.message,
+ stack: status.stack};
+ results_element.textContent = JSON.stringify(data);
+
+ // To avoid a HierarchyRequestError with XML documents, ensure that 'results_element'
+ // is inserted at a location that results in a valid document.
+ var parent = document.body
+ ? document.body // <body> is required in XHTML documents
+ : document.documentElement; // fallback for optional <body> in HTML5, SVG, etc.
+
+ parent.appendChild(results_element);
+}
+
+add_completion_callback(dump_test_results);
+
+/* If the parent window has a testharness_properties object,
+ * we use this to provide the test settings. This is used by the
+ * default in-browser runner to configure the timeout and the
+ * rendering of results
+ */
+try {
+ if (window.opener && "testharness_properties" in window.opener) {
+ /* If we pass the testharness_properties object as-is here without
+ * JSON stringifying and reparsing it, IE fails & emits the message
+ * "Could not complete the operation due to error 80700019".
+ */
+ setup(JSON.parse(JSON.stringify(window.opener.testharness_properties)));
+ }
+} catch (e) {
+}
+// vim: set expandtab shiftwidth=4 tabstop=4:
diff --git a/testing/web-platform/tests/resources/testharnessreport.js.headers b/testing/web-platform/tests/resources/testharnessreport.js.headers
new file mode 100644
index 0000000000..5e8f640c66
--- /dev/null
+++ b/testing/web-platform/tests/resources/testharnessreport.js.headers
@@ -0,0 +1,2 @@
+Content-Type: text/javascript; charset=utf-8
+Cache-Control: max-age=3600