diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /testing/web-platform/tests/resources/test | |
parent | Initial commit. (diff) | |
download | thunderbird-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 '')
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, "&") + .replace(/</g, "<") + .replace(/"/g, """) + .replace(/'/g, "'"); + } + + 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 |