summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/resources
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/resources')
-rw-r--r--testing/web-platform/tests/resources/.htaccess2
-rw-r--r--testing/web-platform/tests/resources/META.yml2
-rw-r--r--testing/web-platform/tests/resources/SVGAnimationTestCase-testharness.js102
-rw-r--r--testing/web-platform/tests/resources/accesskey.js34
-rw-r--r--testing/web-platform/tests/resources/blank.html16
-rw-r--r--testing/web-platform/tests/resources/channel.sub.js1097
-rw-r--r--testing/web-platform/tests/resources/check-layout-th.js252
-rw-r--r--testing/web-platform/tests/resources/check-layout.js245
-rw-r--r--testing/web-platform/tests/resources/chromium/README.md7
-rw-r--r--testing/web-platform/tests/resources/chromium/contacts_manager_mock.js90
-rw-r--r--testing/web-platform/tests/resources/chromium/content-index-helpers.js9
-rw-r--r--testing/web-platform/tests/resources/chromium/enable-hyperlink-auditing.js2
-rw-r--r--testing/web-platform/tests/resources/chromium/fake-hid.js297
-rw-r--r--testing/web-platform/tests/resources/chromium/fake-serial.js443
-rw-r--r--testing/web-platform/tests/resources/chromium/generic_sensor_mocks.js519
-rw-r--r--testing/web-platform/tests/resources/chromium/generic_sensor_mocks.js.headers1
-rw-r--r--testing/web-platform/tests/resources/chromium/mock-barcodedetection.js136
-rw-r--r--testing/web-platform/tests/resources/chromium/mock-barcodedetection.js.headers1
-rw-r--r--testing/web-platform/tests/resources/chromium/mock-battery-monitor.headers1
-rw-r--r--testing/web-platform/tests/resources/chromium/mock-battery-monitor.js61
-rw-r--r--testing/web-platform/tests/resources/chromium/mock-direct-sockets.js94
-rw-r--r--testing/web-platform/tests/resources/chromium/mock-facedetection.js130
-rw-r--r--testing/web-platform/tests/resources/chromium/mock-facedetection.js.headers1
-rw-r--r--testing/web-platform/tests/resources/chromium/mock-idle-detection.js80
-rw-r--r--testing/web-platform/tests/resources/chromium/mock-imagecapture.js309
-rw-r--r--testing/web-platform/tests/resources/chromium/mock-managed-config.js91
-rw-r--r--testing/web-platform/tests/resources/chromium/mock-pressure-service.js134
-rw-r--r--testing/web-platform/tests/resources/chromium/mock-pressure-service.js.headers1
-rw-r--r--testing/web-platform/tests/resources/chromium/mock-subapps.js89
-rw-r--r--testing/web-platform/tests/resources/chromium/mock-textdetection.js92
-rw-r--r--testing/web-platform/tests/resources/chromium/mock-textdetection.js.headers1
-rw-r--r--testing/web-platform/tests/resources/chromium/nfc-mock.js437
-rw-r--r--testing/web-platform/tests/resources/chromium/web-bluetooth-test.js629
-rw-r--r--testing/web-platform/tests/resources/chromium/web-bluetooth-test.js.headers1
-rw-r--r--testing/web-platform/tests/resources/chromium/webusb-child-test.js47
-rw-r--r--testing/web-platform/tests/resources/chromium/webusb-child-test.js.headers1
-rw-r--r--testing/web-platform/tests/resources/chromium/webusb-test.js582
-rw-r--r--testing/web-platform/tests/resources/chromium/webusb-test.js.headers1
-rw-r--r--testing/web-platform/tests/resources/chromium/webxr-test-math-helper.js298
-rw-r--r--testing/web-platform/tests/resources/chromium/webxr-test-math-helper.js.headers1
-rw-r--r--testing/web-platform/tests/resources/chromium/webxr-test.js2128
-rw-r--r--testing/web-platform/tests/resources/chromium/webxr-test.js.headers1
-rw-r--r--testing/web-platform/tests/resources/declarative-shadow-dom-polyfill.js25
-rw-r--r--testing/web-platform/tests/resources/idlharness-shadowrealm.js61
-rw-r--r--testing/web-platform/tests/resources/idlharness.js3553
-rw-r--r--testing/web-platform/tests/resources/idlharness.js.headers2
-rw-r--r--testing/web-platform/tests/resources/readme.md14
-rw-r--r--testing/web-platform/tests/resources/sriharness.js226
-rw-r--r--testing/web-platform/tests/resources/test-only-api.js31
-rw-r--r--testing/web-platform/tests/resources/test-only-api.js.headers2
-rw-r--r--testing/web-platform/tests/resources/test-only-api.m.js5
-rw-r--r--testing/web-platform/tests/resources/test-only-api.m.js.headers2
-rw-r--r--testing/web-platform/tests/resources/test/README.md83
-rw-r--r--testing/web-platform/tests/resources/test/conftest.py269
-rw-r--r--testing/web-platform/tests/resources/test/harness.html26
-rw-r--r--testing/web-platform/tests/resources/test/idl-helper.js24
-rw-r--r--testing/web-platform/tests/resources/test/nested-testharness.js80
-rw-r--r--testing/web-platform/tests/resources/test/requirements.txt1
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/abortsignal.html49
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/add_cleanup.html91
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async.html85
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_bad_return.html50
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_rejection.html94
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_rejection_after_load.html52
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_timeout.html57
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/add_cleanup_bad_return.html61
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/add_cleanup_count.html39
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/add_cleanup_err.html45
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/add_cleanup_err_multi.html52
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/add_cleanup_sync_queue.html55
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/api-tests-1.html991
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/api-tests-2.html62
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/api-tests-3.html34
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/assert-array-equals.html162
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/assert-throws-dom.html55
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/force_timeout.html60
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/generate-callback.html153
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlDictionary/test_partial_interface_of.html90
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_exposed_wildcard.html233
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_immutable_prototype.html298
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_interface_mixin.html131
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_partial_interface_of.html188
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_primary_interface_of.html116
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_to_json_operation.html177
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_attribute.html100
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_operation.html242
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_partial_namespace.html125
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/iframe-callback.html116
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/iframe-consolidate-errors.html50
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/iframe-consolidate-tests.html85
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/iframe-msg.html84
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/log-insertion.html46
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/no-title.html146
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/order.html36
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/promise-async.html172
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/promise-with-sync.html79
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/promise.html219
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/queue.html130
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/setup-function-worker.js14
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/setup-worker-service.html86
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/single-page-test-fail.html28
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/single-page-test-no-assertions.html25
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/single-page-test-no-body.html26
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/single-page-test-pass.html28
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/step_wait.html57
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/step_wait_func.html49
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/task-scheduling-promise-test.html241
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/task-scheduling-test.html141
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/uncaught-exception-handle.html33
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/uncaught-exception-ignore.html35
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/worker-dedicated-uncaught-allow.html45
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/worker-dedicated-uncaught-single.html56
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/worker-dedicated.sub.html88
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/worker-error.js8
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/worker-service.html115
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/worker-shared.html73
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/worker-uncaught-allow.js19
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/worker-uncaught-single.js8
-rw-r--r--testing/web-platform/tests/resources/test/tests/functional/worker.js34
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlArray/is_json_type.html192
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlDictionary/get_reverse_inheritance_stack.html47
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlDictionary/test_partial_dictionary.html39
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlInterface/constructors.html26
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlInterface/default_to_json_operation.html114
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlInterface/do_member_unscopable_asserts.html56
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_interface_object.html22
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_interface_object_owner.html21
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_legacy_namespace.html20
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_qualified_name.html20
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_reverse_inheritance_stack.html49
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlInterface/has_default_to_json_regular_operation.html47
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlInterface/has_to_json_regular_operation.html31
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlInterface/should_have_interface_object.html30
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlInterface/test_primary_interface_of_undefined.html29
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlInterfaceMember/is_to_json_regular_operation.html42
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/IdlInterfaceMember/toString.html36
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/assert_implements.html43
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/assert_implements_optional.html43
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/assert_object_equals.html152
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/async-test-return-restrictions.html135
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/basic.html48
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/exceptional-cases-timeouts.html120
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/exceptional-cases.html392
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/format-value.html123
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/helpers.js21
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/late-test.html54
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/promise_setup-timeout.html28
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/promise_setup.html333
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/single_test.html94
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/test-return-restrictions.html156
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/throwing-assertions.html268
-rw-r--r--testing/web-platform/tests/resources/test/tests/unit/unpaired-surrogates.html143
-rw-r--r--testing/web-platform/tests/resources/test/tox.ini13
-rw-r--r--testing/web-platform/tests/resources/test/wptserver.py58
-rw-r--r--testing/web-platform/tests/resources/testdriver-actions.js599
-rw-r--r--testing/web-platform/tests/resources/testdriver-vendor.js0
-rw-r--r--testing/web-platform/tests/resources/testdriver-vendor.js.headers2
-rw-r--r--testing/web-platform/tests/resources/testdriver.js809
-rw-r--r--testing/web-platform/tests/resources/testdriver.js.headers2
-rw-r--r--testing/web-platform/tests/resources/testharness.js4933
-rw-r--r--testing/web-platform/tests/resources/testharness.js.headers2
-rw-r--r--testing/web-platform/tests/resources/testharnessreport.js57
-rw-r--r--testing/web-platform/tests/resources/testharnessreport.js.headers2
-rwxr-xr-xtesting/web-platform/tests/resources/webidl2/build.sh12
-rw-r--r--testing/web-platform/tests/resources/webidl2/lib/README.md4
-rw-r--r--testing/web-platform/tests/resources/webidl2/lib/VERSION.md1
-rw-r--r--testing/web-platform/tests/resources/webidl2/lib/webidl2.js3824
-rw-r--r--testing/web-platform/tests/resources/webidl2/lib/webidl2.js.headers1
168 files changed, 32481 insertions, 0 deletions
diff --git a/testing/web-platform/tests/resources/.htaccess b/testing/web-platform/tests/resources/.htaccess
new file mode 100644
index 0000000000..fd46101ca0
--- /dev/null
+++ b/testing/web-platform/tests/resources/.htaccess
@@ -0,0 +1,2 @@
+# make tests that use utf-16 not inherit the encoding for testharness.js et. al.
+AddCharset utf-8 .css .js
diff --git a/testing/web-platform/tests/resources/META.yml b/testing/web-platform/tests/resources/META.yml
new file mode 100644
index 0000000000..64a240ccbe
--- /dev/null
+++ b/testing/web-platform/tests/resources/META.yml
@@ -0,0 +1,2 @@
+suggested_reviewers:
+ - jgraham
diff --git a/testing/web-platform/tests/resources/SVGAnimationTestCase-testharness.js b/testing/web-platform/tests/resources/SVGAnimationTestCase-testharness.js
new file mode 100644
index 0000000000..9ebaf68039
--- /dev/null
+++ b/testing/web-platform/tests/resources/SVGAnimationTestCase-testharness.js
@@ -0,0 +1,102 @@
+// NOTE(edvardt):
+// This file is a slimmed down wrapper for the old SVGAnimationTestCase.js,
+// it has some convenience functions and should not be used for new tests.
+// New tests should not build on this API as it's just meant to keep things
+// working.
+
+// Helper functions
+const xlinkNS = "http://www.w3.org/1999/xlink"
+
+function expectFillColor(element, red, green, blue, message) {
+ let color = window.getComputedStyle(element, null).fill;
+ var re = new RegExp("rgba?\\(([^, ]*), ([^, ]*), ([^, ]*)(?:, )?([^, ]*)\\)");
+ rgb = re.exec(color);
+
+ assert_approx_equals(Number(rgb[1]), red, 2.0, message);
+ assert_approx_equals(Number(rgb[2]), green, 2.0, message);
+ assert_approx_equals(Number(rgb[3]), blue, 2.0, message);
+}
+
+function expectColor(element, red, green, blue, property) {
+ if (typeof property != "string")
+ color = getComputedStyle(element).getPropertyValue("color");
+ else
+ color = getComputedStyle(element).getPropertyValue(property);
+ var re = new RegExp("rgba?\\(([^, ]*), ([^, ]*), ([^, ]*)(?:, )?([^, ]*)\\)");
+ rgb = re.exec(color);
+ assert_approx_equals(Number(rgb[1]), red, 2.0);
+ assert_approx_equals(Number(rgb[2]), green, 2.0);
+ assert_approx_equals(Number(rgb[3]), blue, 2.0);
+}
+
+function createSVGElement(type) {
+ return document.createElementNS("http://www.w3.org/2000/svg", type);
+}
+
+// Inspired by Layoutests/animations/animation-test-helpers.js
+function moveAnimationTimelineAndSample(index) {
+ var animationId = expectedResults[index][0];
+ var time = expectedResults[index][1];
+ var sampleCallback = expectedResults[index][2];
+ var animation = rootSVGElement.ownerDocument.getElementById(animationId);
+
+ // If we want to sample the animation end, add a small delta, to reliable point past the end of the animation.
+ newTime = time;
+
+ // The sample time is relative to the start time of the animation, take that into account.
+ rootSVGElement.setCurrentTime(newTime);
+
+ // NOTE(edvardt):
+ // This is a dumb hack, some of the old tests sampled before the animation start, this
+ // isn't technically part of the animation tests and is "impossible" to translate since
+ // tests start automatically. Thus I solved it by skipping it.
+ if (time != 0.0)
+ sampleCallback();
+}
+
+var currentTest = 0;
+var expectedResults;
+
+function sampleAnimation(t) {
+ if (currentTest == expectedResults.length) {
+ t.done();
+ return;
+ }
+
+ moveAnimationTimelineAndSample(currentTest);
+ ++currentTest;
+
+ step_timeout(t.step_func(function () { sampleAnimation(t); }), 0);
+}
+
+function runAnimationTest(t, expected) {
+ if (!expected)
+ throw("Expected results are missing!");
+ if (currentTest > 0)
+ throw("Not allowed to call runAnimationTest() twice");
+
+ expectedResults = expected;
+ testCount = expectedResults.length;
+ currentTest = 0;
+
+ step_timeout(t.step_func(function () { sampleAnimation(this); }), 50);
+}
+
+function smil_async_test(func) {
+ async_test(t => {
+ window.onload = t.step_func(function () {
+ // Pause animations, we'll drive them manually.
+ // This also ensures that the timeline is paused before
+ // it starts. This should make the instance time of the below
+ // 'click' (for instance) 0, and hence minimize rounding
+ // errors for the addition in moveAnimationTimelineAndSample.
+ rootSVGElement.pauseAnimations();
+
+ // If eg. an animation is running with begin="0s", and
+ // we want to sample the first time, before the animation
+ // starts, then we can't delay the testing by using an
+ // onclick event, as the animation would be past start time.
+ func(t);
+ });
+ });
+}
diff --git a/testing/web-platform/tests/resources/accesskey.js b/testing/web-platform/tests/resources/accesskey.js
new file mode 100644
index 0000000000..e95c9d21e5
--- /dev/null
+++ b/testing/web-platform/tests/resources/accesskey.js
@@ -0,0 +1,34 @@
+/*
+ * Function that sends an accesskey using the proper key combination depending on the browser and OS.
+ *
+ * This needs that the test imports the following scripts:
+ * <script src="/resources/testdriver.js"></script>
+ * <script src="/resources/testdriver-actions.js"></script>
+ * <script src="/resources/testdriver-vendor.js"></script>
+*/
+function pressAccessKey(accessKey){
+ let controlKey = '\uE009'; // left Control key
+ let altKey = '\uE00A'; // left Alt key
+ let optionKey = altKey; // left Option key
+ let shiftKey = '\uE008'; // left Shift key
+ // There are differences in using accesskey across browsers and OS's.
+ // See: // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/accesskey
+ let isMacOSX = navigator.userAgent.indexOf("Mac") != -1;
+ let osAccessKey = isMacOSX ? [controlKey, optionKey] : [shiftKey, altKey];
+ let actions = new test_driver.Actions();
+ // Press keys.
+ for (let key of osAccessKey) {
+ actions = actions.keyDown(key);
+ }
+ actions = actions
+ .keyDown(accessKey)
+ .addTick()
+ .keyUp(accessKey);
+ osAccessKey.reverse();
+ for (let key of osAccessKey) {
+ actions = actions.keyUp(key);
+ }
+ return actions.send();
+}
+
+
diff --git a/testing/web-platform/tests/resources/blank.html b/testing/web-platform/tests/resources/blank.html
new file mode 100644
index 0000000000..edeaa45bb6
--- /dev/null
+++ b/testing/web-platform/tests/resources/blank.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title>Blank Page</title>
+ <script>
+ window.onload = function(event) {
+ // This is needed to ensure the onload event fires when this page is
+ // opened as a popup.
+ // See https://github.com/web-platform-tests/wpt/pull/18157
+ };
+ </script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/resources/channel.sub.js b/testing/web-platform/tests/resources/channel.sub.js
new file mode 100644
index 0000000000..370d4f5905
--- /dev/null
+++ b/testing/web-platform/tests/resources/channel.sub.js
@@ -0,0 +1,1097 @@
+(function() {
+ function randInt(bits) {
+ if (bits < 1 || bits > 53) {
+ throw new TypeError();
+ } else {
+ if (bits >= 1 && bits <= 30) {
+ return 0 | ((1 << bits) * Math.random());
+ } else {
+ var high = (0 | ((1 << (bits - 30)) * Math.random())) * (1 << 30);
+ var low = 0 | ((1 << 30) * Math.random());
+ return high + low;
+ }
+ }
+ }
+
+
+ function toHex(x, length) {
+ var rv = x.toString(16);
+ while (rv.length < length) {
+ rv = "0" + rv;
+ }
+ return rv;
+ }
+
+ function createUuid() {
+ return [toHex(randInt(32), 8),
+ toHex(randInt(16), 4),
+ toHex(0x4000 | randInt(12), 4),
+ toHex(0x8000 | randInt(14), 4),
+ toHex(randInt(48), 12)].join("-");
+ }
+
+
+ /**
+ * Cache of WebSocket instances per channel
+ *
+ * For reading there can only be one channel with each UUID, so we
+ * just have a simple map of {uuid: WebSocket}. The socket can be
+ * closed when the channel is closed.
+ *
+ * For writing there can be many channels for each uuid. Those can
+ * share a websocket (within a specific global), so we have a map
+ * of {uuid: [WebSocket, count]}. Count is incremented when a
+ * channel is opened with a given uuid, and decremented when its
+ * closed. When the count reaches zero we can close the underlying
+ * socket.
+ */
+ class SocketCache {
+ constructor() {
+ this.readSockets = new Map();
+ this.writeSockets = new Map();
+ };
+
+ async getOrCreate(type, uuid, onmessage=null) {
+ function createSocket() {
+ let protocol = self.isSecureContext ? "wss" : "ws";
+ let port = self.isSecureContext? "{{ports[wss][0]}}" : "{{ports[ws][0]}}";
+ let url = `${protocol}://{{host}}:${port}/msg_channel?uuid=${uuid}&direction=${type}`;
+ let socket = new WebSocket(url);
+ if (onmessage !== null) {
+ socket.onmessage = onmessage;
+ };
+ return new Promise(resolve => socket.addEventListener("open", () => resolve(socket)));
+ }
+
+ let socket;
+ if (type === "read") {
+ if (this.readSockets.has(uuid)) {
+ throw new Error("Can't create multiple read sockets with same UUID");
+ }
+ socket = await createSocket();
+ // If the socket is closed by the server, ensure it's removed from the cache
+ socket.addEventListener("close", () => this.readSockets.delete(uuid));
+ this.readSockets.set(uuid, socket);
+ } else if (type === "write") {
+ let count;
+ if (onmessage !== null) {
+ throw new Error("Can't set message handler for write sockets");
+ }
+ if (this.writeSockets.has(uuid)) {
+ [socket, count] = this.writeSockets.get(uuid);
+ } else {
+ socket = await createSocket();
+ count = 0;
+ }
+ count += 1;
+ // If the socket is closed by the server, ensure it's removed from the cache
+ socket.addEventListener("close", () => this.writeSockets.delete(uuid));
+ this.writeSockets.set(uuid, [socket, count]);
+ } else {
+ throw new Error(`Unknown type ${type}`);
+ }
+ return socket;
+ };
+
+ async close(type, uuid) {
+ let target = type === "read" ? this.readSockets : this.writeSockets;
+ const data = target.get(uuid);
+ if (!data) {
+ return;
+ }
+ let count, socket;
+ if (type == "read") {
+ socket = data;
+ count = 0;
+ } else if (type === "write") {
+ [socket, count] = data;
+ count -= 1;
+ if (count > 0) {
+ target.set(uuid, [socket, count]);
+ }
+ };
+ if (count <= 0 && socket) {
+ target.delete(uuid);
+ socket.close(1000);
+ await new Promise(resolve => socket.addEventListener("close", resolve));
+ }
+ };
+
+ async closeAll() {
+ let sockets = [];
+ this.readSockets.forEach(value => sockets.push(value));
+ this.writeSockets.forEach(value => sockets.push(value[0]));
+ let closePromises = sockets.map(socket =>
+ new Promise(resolve => socket.addEventListener("close", resolve)));
+ sockets.forEach(socket => socket.close(1000));
+ this.readSockets.clear();
+ this.writeSockets.clear();
+ await Promise.all(closePromises);
+ }
+ }
+
+ const socketCache = new SocketCache();
+
+ /**
+ * Abstract base class for objects that allow sending / receiving
+ * messages over a channel.
+ */
+ class Channel {
+ type = null;
+
+ constructor(uuid) {
+ /** UUID for the channel */
+ this.uuid = uuid;
+ this.socket = null;
+ this.eventListeners = {
+ connect: new Set(),
+ close: new Set()
+ };
+ }
+
+ hasConnection() {
+ return this.socket !== null && this.socket.readyState <= WebSocket.OPEN;
+ }
+
+ /**
+ * Connect to the channel.
+ *
+ * @param {Function} onmessage - Event handler function for
+ * the underlying websocket message.
+ */
+ async connect(onmessage) {
+ if (this.hasConnection()) {
+ return;
+ }
+ this.socket = await socketCache.getOrCreate(this.type, this.uuid, onmessage);
+ this._dispatch("connect");
+ }
+
+ /**
+ * Close the channel and underlying websocket connection
+ */
+ async close() {
+ this.socket = null;
+ await socketCache.close(this.type, this.uuid);
+ this._dispatch("close");
+ }
+
+ /**
+ * Add an event callback function. Supported message types are
+ * "connect", "close", and "message" (for ``RecvChannel``).
+ *
+ * @param {string} type - Message type.
+ * @param {Function} fn - Callback function. This is called
+ * with an event-like object, with ``type`` and ``data``
+ * properties.
+ */
+ addEventListener(type, fn) {
+ if (typeof type !== "string") {
+ throw new TypeError(`Expected string, got ${typeof type}`);
+ }
+ if (typeof fn !== "function") {
+ throw new TypeError(`Expected function, got ${typeof fn}`);
+ }
+ if (!this.eventListeners.hasOwnProperty(type)) {
+ throw new Error(`Unrecognised event type ${type}`);
+ }
+ this.eventListeners[type].add(fn);
+ };
+
+ /**
+ * Remove an event callback function.
+ *
+ * @param {string} type - Event type.
+ * @param {Function} fn - Callback function to remove.
+ */
+ removeEventListener(type, fn) {
+ if (!typeof type === "string") {
+ throw new TypeError(`Expected string, got ${typeof type}`);
+ }
+ if (typeof fn !== "function") {
+ throw new TypeError(`Expected function, got ${typeof fn}`);
+ }
+ let listeners = this.eventListeners[type];
+ if (listeners) {
+ listeners.delete(fn);
+ }
+ };
+
+ _dispatch(type, data) {
+ let listeners = this.eventListeners[type];
+ if (listeners) {
+ // If any listener throws we end up not calling the other
+ // listeners. This hopefully makes debugging easier, but
+ // is different to DOM event listeners.
+ listeners.forEach(fn => fn({type, data}));
+ }
+ };
+
+ }
+
+ /**
+ * Send messages over a channel
+ */
+ class SendChannel extends Channel {
+ type = "write";
+
+ /**
+ * Connect to the channel. Automatically called when sending the
+ * first message.
+ */
+ async connect() {
+ return super.connect(null);
+ }
+
+ async _send(cmd, body=null) {
+ if (!this.hasConnection()) {
+ await this.connect();
+ }
+ this.socket.send(JSON.stringify([cmd, body]));
+ }
+
+ /**
+ * Send a message. The message object must be JSON-serializable.
+ *
+ * @param {Object} msg - The message object to send.
+ */
+ async send(msg) {
+ await this._send("message", msg);
+ }
+
+ /**
+ * Disconnect the associated `RecvChannel <#RecvChannel>`_, if
+ * any, on the server side.
+ */
+ async disconnectReader() {
+ await this._send("disconnectReader");
+ }
+
+ /**
+ * Disconnect this channel on the server side.
+ */
+ async delete() {
+ await this._send("delete");
+ }
+ };
+ self.SendChannel = SendChannel;
+
+ const recvChannelsCreated = new Set();
+
+ /**
+ * Receive messages over a channel
+ */
+ class RecvChannel extends Channel {
+ type = "read";
+
+ constructor(uuid) {
+ if (recvChannelsCreated.has(uuid)) {
+ throw new Error(`Already created RecvChannel with id ${uuid}`);
+ }
+ super(uuid);
+ this.eventListeners.message = new Set();
+ }
+
+ async connect() {
+ if (this.hasConnection()) {
+ return;
+ }
+ await super.connect(event => this.readMessage(event.data));
+ }
+
+ readMessage(data) {
+ let msg = JSON.parse(data);
+ this._dispatch("message", msg);
+ }
+
+ /**
+ * Wait for the next message and return it (after passing it to
+ * existing handlers)
+ *
+ * @returns {Promise} - Promise that resolves to the message data.
+ */
+ nextMessage() {
+ return new Promise(resolve => {
+ let fn = ({data}) => {
+ this.removeEventListener("message", fn);
+ resolve(data);
+ };
+ this.addEventListener("message", fn);
+ });
+ }
+ }
+
+ /**
+ * Create a new channel pair
+ *
+ * @returns {Array} - Array of [RecvChannel, SendChannel] for the same channel.
+ */
+ self.channel = function() {
+ let uuid = createUuid();
+ let recvChannel = new RecvChannel(uuid);
+ let sendChannel = new SendChannel(uuid);
+ return [recvChannel, sendChannel];
+ };
+
+ /**
+ * Create an unconnected channel defined by a `uuid` in
+ * ``location.href`` for listening for `RemoteGlobal
+ * <#RemoteGlobal>`_ messages.
+ *
+ * @returns {RemoteGlobalCommandRecvChannel} - Disconnected channel
+ */
+ self.global_channel = function() {
+ let uuid = new URLSearchParams(location.search).get("uuid");
+ if (!uuid) {
+ throw new Error("URL must have a uuid parameter to use as a RemoteGlobal");
+ }
+ return new RemoteGlobalCommandRecvChannel(new RecvChannel(uuid));
+ };
+
+ /**
+ * Start listening for `RemoteGlobal <#RemoteGlobal>`_ messages on
+ * a channel defined by a `uuid` in `location.href`
+ *
+ * @returns {RemoteGlobalCommandRecvChannel} - Connected channel
+ */
+ self.start_global_channel = async function() {
+ let channel = self.global_channel();
+ await channel.connect();
+ return channel;
+ };
+
+ /**
+ * Close all WebSockets used by channels in the current realm.
+ *
+ */
+ self.close_all_channel_sockets = async function() {
+ await socketCache.closeAll();
+ // Spinning the event loop after the close events is necessary to
+ // ensure that the channels really are closed and don't affect
+ // bfcache behaviour in at least some implementations.
+ await new Promise(resolve => setTimeout(resolve, 0));
+ };
+
+ /**
+ * Handler for `RemoteGlobal <#RemoteGlobal>`_ commands.
+ *
+ * This can't be constructed directly but must be obtained from
+ * `global_channel() <#global_channel>`_ or
+ * `start_global_channel() <#start_global_channel>`_.
+ */
+ class RemoteGlobalCommandRecvChannel {
+ constructor(recvChannel) {
+ this.channel = recvChannel;
+ this.uuid = recvChannel.uuid;
+ this.channel.addEventListener("message", ({data}) => this.handleMessage(data));
+ this.messageHandlers = new Set();
+ };
+
+ /**
+ * Connect to the channel and start handling messages.
+ */
+ async connect() {
+ await this.channel.connect();
+ }
+
+ /**
+ * Close the channel and underlying websocket connection
+ */
+ async close() {
+ await this.channel.close();
+ }
+
+ async handleMessage(msg) {
+ const {id, command, params, respChannel} = msg;
+ let result = {};
+ let resp = {id, result};
+ if (command === "call") {
+ const fn = deserialize(params.fn);
+ const args = params.args.map(deserialize);
+ try {
+ let resultValue = await fn(...args);
+ result.result = serialize(resultValue);
+ } catch(e) {
+ let exception = serialize(e);
+ const getAsInt = (obj, prop) => {
+ let value = prop in obj ? parseInt(obj[prop]) : 0;
+ return Number.isNaN(value) ? 0 : value;
+ };
+ result.exceptionDetails = {
+ text: e.toString(),
+ lineNumber: getAsInt(e, "lineNumber"),
+ columnNumber: getAsInt(e, "columnNumber"),
+ exception
+ };
+ }
+ } else if (command === "postMessage") {
+ this.messageHandlers.forEach(fn => fn(deserialize(params.msg)));
+ }
+ if (respChannel) {
+ let chan = deserialize(respChannel);
+ await chan.connect();
+ await chan.send(resp);
+ }
+ }
+
+ /**
+ * Add a handler for ``postMessage`` messages
+ *
+ * @param {Function} fn - Callback function that receives the
+ * message.
+ */
+ addMessageHandler(fn) {
+ this.messageHandlers.add(fn);
+ }
+
+ /**
+ * Remove a handler for ``postMessage`` messages
+ *
+ * @param {Function} fn - Callback function to remove
+ */
+ removeMessageHandler(fn) {
+ this.messageHandlers.delete(fn);
+ }
+
+ /**
+ * Wait for the next ``postMessage`` message and return it
+ * (after passing it to existing handlers)
+ *
+ * @returns {Promise} - Promise that resolves to the message.
+ */
+ nextMessage() {
+ return new Promise(resolve => {
+ let fn = (msg) => {
+ this.removeMessageHandler(fn);
+ resolve(msg);
+ };
+ this.addMessageHandler(fn);
+ });
+ }
+ }
+
+ class RemoteGlobalResponseRecvChannel {
+ constructor(recvChannel) {
+ this.channel = recvChannel;
+ this.channel.addEventListener("message", ({data}) => this.handleMessage(data));
+ this.responseHandlers = new Map();
+ }
+
+ setResponseHandler(commandId, fn) {
+ this.responseHandlers.set(commandId, fn);
+ }
+
+ handleMessage(msg) {
+ let {id, result} = msg;
+ let handler = this.responseHandlers.get(id);
+ if (handler) {
+ this.responseHandlers.delete(id);
+ handler(result);
+ }
+ }
+
+ close() {
+ return this.channel.close();
+ }
+ }
+
+ /**
+ * Object representing a remote global that has a
+ * `RemoteGlobalCommandRecvChannel
+ * <#RemoteGlobalCommandRecvChannel>`_
+ */
+ class RemoteGlobal {
+ /**
+ * Create a new RemoteGlobal object.
+ *
+ * This doesn't actually construct the global itself; that
+ * must be done elsewhere, with a ``uuid`` query parameter in
+ * its URL set to the same as the ``uuid`` property of this
+ * object.
+ *
+ * @param {SendChannel|string} [dest] - Either a SendChannel
+ * to the destination, or the UUID of the destination. If
+ * ommitted, a new UUID is generated, which can be used when
+ * constructing the URL for the global.
+ *
+ */
+ constructor(dest) {
+ if (dest === undefined || dest === null) {
+ dest = createUuid();
+ }
+ if (typeof dest == "string") {
+ /** UUID for the global */
+ this.uuid = dest;
+ this.sendChannel = new SendChannel(dest);
+ } else if (dest instanceof SendChannel) {
+ this.sendChannel = dest;
+ this.uuid = dest.uuid;
+ } else {
+ throw new TypeError("Unrecognised type, expected string or SendChannel");
+ }
+ this.recvChannel = null;
+ this.respChannel = null;
+ this.connected = false;
+ this.commandId = 0;
+ }
+
+ /**
+ * Connect to the channel. Automatically called when sending the
+ * first message
+ */
+ async connect() {
+ if (this.connected) {
+ return;
+ }
+ let [recvChannel, respChannel] = self.channel();
+ await Promise.all([this.sendChannel.connect(), recvChannel.connect()]);
+ this.recvChannel = new RemoteGlobalResponseRecvChannel(recvChannel);
+ this.respChannel = respChannel;
+ this.connected = true;
+ }
+
+ async sendMessage(command, params, hasResp=true) {
+ if (!this.connected) {
+ await this.connect();
+ }
+ let msg = {id: this.commandId++, command, params};
+ if (hasResp) {
+ msg.respChannel = serialize(this.respChannel);
+ }
+ let response;
+ if (hasResp) {
+ response = new Promise(resolve =>
+ this.recvChannel.setResponseHandler(msg.id, resolve));
+ } else {
+ response = null;
+ }
+ this.sendChannel.send(msg);
+ return await response;
+ }
+
+ /**
+ * Run the function ``fn`` in the remote global, passing arguments
+ * ``args``, and return the result after awaiting any returned
+ * promise.
+ *
+ * @param {Function} fn - Function to run in the remote global.
+ * @param {...Any} args - Arguments to pass to the function
+ * @returns {Promise} - Promise resolving to the return value
+ * of the function.
+ */
+ async call(fn, ...args) {
+ let result = await this.sendMessage("call", {fn: serialize(fn), args: args.map(x => serialize(x))}, true);
+ if (result.exceptionDetails) {
+ throw deserialize(result.exceptionDetails.exception);
+ }
+ return deserialize(result.result);
+ }
+
+ /**
+ * Post a message to the remote
+ *
+ * @param {Any} msg - The message to send.
+ */
+ async postMessage(msg) {
+ await this.sendMessage("postMessage", {msg: serialize(msg)}, false);
+ }
+
+ /**
+ * Disconnect the associated `RemoteGlobalCommandRecvChannel
+ * <#RemoteGlobalCommandRecvChannel>`_, if any, on the server
+ * side.
+ *
+ * @returns {Promise} - Resolved once the channel is disconnected.
+ */
+ disconnectReader() {
+ // This causes any readers to disconnect until they are explicitly reconnected
+ return this.sendChannel.disconnectReader();
+ }
+
+ /**
+ * Close the channel and underlying websocket connections
+ */
+ close() {
+ let closers = [this.sendChannel.close()];
+ if (this.recvChannel !== null) {
+ closers.push(this.recvChannel.close());
+ }
+ if (this.respChannel !== null) {
+ closers.push(this.respChannel.close());
+ }
+ return Promise.all(closers);
+ }
+ }
+
+ self.RemoteGlobal = RemoteGlobal;
+
+ function typeName(value) {
+ let type = typeof value;
+ if (type === "undefined" ||
+ type === "string" ||
+ type === "boolean" ||
+ type === "number" ||
+ type === "bigint" ||
+ type === "symbol" ||
+ type === "function") {
+ return type;
+ }
+
+ if (value === null) {
+ return "null";
+ }
+ // The handling of cross-global objects here is broken
+ if (value instanceof RemoteObject) {
+ return "remoteobject";
+ }
+ if (value instanceof SendChannel) {
+ return "sendchannel";
+ }
+ if (value instanceof RecvChannel) {
+ return "recvchannel";
+ }
+ if (value instanceof Error) {
+ return "error";
+ }
+ if (Array.isArray(value)) {
+ return "array";
+ }
+ let constructor = value.constructor && value.constructor.name;
+ if (constructor === "RegExp" ||
+ constructor === "Date" ||
+ constructor === "Map" ||
+ constructor === "Set" ||
+ constructor == "WeakMap" ||
+ constructor == "WeakSet") {
+ return constructor.toLowerCase();
+ }
+ // The handling of cross-global objects here is broken
+ if (typeof window == "object" && window === self) {
+ if (value instanceof Element) {
+ return "element";
+ }
+ if (value instanceof Document) {
+ return "document";
+ }
+ if (value instanceof Node) {
+ return "node";
+ }
+ if (value instanceof Window) {
+ return "window";
+ }
+ }
+ if (Promise.resolve(value) === value) {
+ return "promise";
+ }
+ return "object";
+ }
+
+ let remoteObjectsById = new Map();
+
+ function remoteId(obj) {
+ let rv;
+ rv = createUuid();
+ remoteObjectsById.set(rv, obj);
+ return rv;
+ }
+
+ /**
+ * Representation of a non-primitive type passed through a channel
+ */
+ class RemoteObject {
+ constructor(type, objectId) {
+ this.type = type;
+ this.objectId = objectId;
+ }
+
+ /**
+ * Create a RemoteObject containing a handle to reference obj
+ *
+ * @param {Any} obj - The object to reference.
+ */
+ static from(obj) {
+ let type = typeName(obj);
+ let id = remoteId(obj);
+ return new RemoteObject(type, id);
+ }
+
+ /**
+ * Return the local object referenced by the ``objectId`` of
+ * this ``RemoteObject``, or ``null`` if there isn't a such an
+ * object in this realm.
+ */
+ toLocal() {
+ if (remoteObjectsById.has(this.objectId)) {
+ return remoteObjectsById.get(this.objectId);
+ }
+ return null;
+ }
+
+ /**
+ * Remove the object from the local cache. This means that future
+ * calls to ``toLocal`` with the same objectId will always return
+ * ``null``.
+ */
+ delete() {
+ remoteObjectsById.delete(this.objectId);
+ }
+ }
+
+ self.RemoteObject = RemoteObject;
+
+ /**
+ * Serialize an object as a JSON-compatible representation.
+ *
+ * The format used is similar (but not identical to)
+ * `WebDriver-BiDi
+ * <https://w3c.github.io/webdriver-bidi/#data-types-protocolValue>`_.
+ *
+ * Each item to be serialized can have the following fields:
+ *
+ * type - The name of the type being represented e.g. "string", or
+ * "map". For primitives this matches ``typeof``, but for
+ * ``object`` types that have particular support in the protocol
+ * e.g. arrays and maps, it is a custom value.
+ *
+ * value - A serialized representation of the object value. For
+ * container types this is a JSON container (i.e. an object or an
+ * array) containing a serialized representation of the child
+ * values.
+ *
+ * objectId - An integer used to handle object graphs. Where
+ * an object is present more than once in the serialization, the
+ * first instance has both ``value`` and ``objectId`` fields, but
+ * when encountered again, only ``objectId`` is present, with the
+ * same value as the first instance of the object.
+ *
+ * @param {Any} inValue - The value to be serialized.
+ * @returns {Object} - The serialized object value.
+ */
+ function serialize(inValue) {
+ const queue = [{item: inValue}];
+ let outValue = null;
+
+ // Map from container object input to output value
+ let objectsSeen = new Map();
+ let lastObjectId = 0;
+
+ /* Instead of making this recursive, use a queue holding the objects to be
+ * serialized. Each item in the queue can have the following properties:
+ *
+ * item (required) - the input item to be serialized
+ *
+ * target - For collections, the output serialized object to
+ * which the serialization of the current item will be added.
+ *
+ * targetName - For serializing object members, the name of
+ * the property. For serializing maps either "key" or "value",
+ * depending on whether the item represents a key or a value
+ * in the map.
+ */
+ while (queue.length > 0) {
+ const {item, target, targetName} = queue.shift();
+ let type = typeName(item);
+
+ let serialized = {type};
+
+ if (objectsSeen.has(item)) {
+ let outputValue = objectsSeen.get(item);
+ if (!outputValue.hasOwnProperty("objectId")) {
+ outputValue.objectId = lastObjectId++;
+ }
+ serialized.objectId = outputValue.objectId;
+ } else {
+ switch (type) {
+ case "undefined":
+ case "null":
+ break;
+ case "string":
+ case "boolean":
+ serialized.value = item;
+ break;
+ case "number":
+ if (item !== item) {
+ serialized.value = "NaN";
+ } else if (item === 0 && 1/item == Number.NEGATIVE_INFINITY) {
+ serialized.value = "-0";
+ } else if (item === Number.POSITIVE_INFINITY) {
+ serialized.value = "+Infinity";
+ } else if (item === Number.NEGATIVE_INFINITY) {
+ serialized.value = "-Infinity";
+ } else {
+ serialized.value = item;
+ }
+ break;
+ case "bigint":
+ case "function":
+ serialized.value = item.toString();
+ break;
+ case "remoteobject":
+ serialized.value = {
+ type: item.type,
+ objectId: item.objectId
+ };
+ break;
+ case "sendchannel":
+ serialized.value = item.uuid;
+ break;
+ case "regexp":
+ serialized.value = {
+ pattern: item.source,
+ flags: item.flags
+ };
+ break;
+ case "date":
+ serialized.value = Date.prototype.toJSON.call(item);
+ break;
+ case "error":
+ serialized.value = {
+ type: item.constructor.name,
+ name: item.name,
+ message: item.message,
+ lineNumber: item.lineNumber,
+ columnNumber: item.columnNumber,
+ fileName: item.fileName,
+ stack: item.stack,
+ };
+ break;
+ case "array":
+ case "set":
+ serialized.value = [];
+ for (let child of item) {
+ queue.push({item: child, target: serialized});
+ }
+ break;
+ case "object":
+ serialized.value = {};
+ for (let [targetName, child] of Object.entries(item)) {
+ queue.push({item: child, target: serialized, targetName});
+ }
+ break;
+ case "map":
+ serialized.value = [];
+ for (let [childKey, childValue] of item.entries()) {
+ queue.push({item: childKey, target: serialized, targetName: "key"});
+ queue.push({item: childValue, target: serialized, targetName: "value"});
+ }
+ break;
+ default:
+ throw new TypeError(`Can't serialize value of type ${type}; consider using RemoteObject.from() to wrap the object`);
+ };
+ }
+ if (serialized.objectId === undefined) {
+ objectsSeen.set(item, serialized);
+ }
+
+ if (target === undefined) {
+ if (outValue !== null) {
+ throw new Error("Tried to create multiple output values");
+ }
+ outValue = serialized;
+ } else {
+ switch (target.type) {
+ case "array":
+ case "set":
+ target.value.push(serialized);
+ break;
+ case "object":
+ target.value[targetName] = serialized;
+ break;
+ case "map":
+ // We always serialize key and value as adjacent items in the queue,
+ // so when we get the key push a new output array and then the value will
+ // be added on the next iteration.
+ if (targetName === "key") {
+ target.value.push([]);
+ }
+ target.value[target.value.length - 1].push(serialized);
+ break;
+ default:
+ throw new Error(`Unknown collection target type ${target.type}`);
+ }
+ }
+ }
+ return outValue;
+ }
+
+ /**
+ * Deserialize an object from a JSON-compatible representation.
+ *
+ * For details on the serialized representation see serialize().
+ *
+ * @param {Object} obj - The value to be deserialized.
+ * @returns {Any} - The deserialized value.
+ */
+ function deserialize(obj) {
+ let deserialized = null;
+ let queue = [{item: obj, target: null}];
+ let objectMap = new Map();
+
+ /* Instead of making this recursive, use a queue holding the objects to be
+ * deserialized. Each item in the queue has the following properties:
+ *
+ * item - The input item to be deserialised.
+ *
+ * target - For members of a collection, a wrapper around the
+ * output collection. This has a ``type`` field which is the
+ * name of the collection type, and a ``value`` field which is
+ * the actual output collection. For primitives, this is null.
+ *
+ * targetName - For object members, the property name on the
+ * output object. For maps, "key" if the item is a key in the output map,
+ * or "value" if it's a value in the output map.
+ */
+ while (queue.length > 0) {
+ const {item, target, targetName} = queue.shift();
+ const {type, value, objectId} = item;
+ let result;
+ let newTarget;
+ if (objectId !== undefined && value === undefined) {
+ result = objectMap.get(objectId);
+ } else {
+ switch(type) {
+ case "undefined":
+ result = undefined;
+ break;
+ case "null":
+ result = null;
+ break;
+ case "string":
+ case "boolean":
+ result = value;
+ break;
+ case "number":
+ if (typeof value === "string") {
+ switch(value) {
+ case "NaN":
+ result = NaN;
+ break;
+ case "-0":
+ result = -0;
+ break;
+ case "+Infinity":
+ result = Number.POSITIVE_INFINITY;
+ break;
+ case "-Infinity":
+ result = Number.NEGATIVE_INFINITY;
+ break;
+ default:
+ throw new Error(`Unexpected number value "${value}"`);
+ }
+ } else {
+ result = value;
+ }
+ break;
+ case "bigint":
+ result = BigInt(value);
+ break;
+ case "function":
+ result = new Function("...args", `return (${value}).apply(null, args)`);
+ break;
+ case "remoteobject":
+ let remote = new RemoteObject(value.type, value.objectId);
+ let local = remote.toLocal();
+ if (local !== null) {
+ result = local;
+ } else {
+ result = remote;
+ }
+ break;
+ case "sendchannel":
+ result = new SendChannel(value);
+ break;
+ case "regexp":
+ result = new RegExp(value.pattern, value.flags);
+ break;
+ case "date":
+ result = new Date(value);
+ break;
+ case "error":
+ // The item.value.type property is the name of the error constructor.
+ // If we have a constructor with the same name in the current realm,
+ // construct an instance of that type, otherwise use a generic Error
+ // type.
+ if (item.value.type in self &&
+ typeof self[item.value.type] === "function") {
+ result = new self[item.value.type](item.value.message);
+ } else {
+ result = new Error(item.value.message);
+ }
+ result.name = item.value.name;
+ result.lineNumber = item.value.lineNumber;
+ result.columnNumber = item.value.columnNumber;
+ result.fileName = item.value.fileName;
+ result.stack = item.value.stack;
+ break;
+ case "array":
+ result = [];
+ newTarget = {type, value: result};
+ for (let child of value) {
+ queue.push({item: child, target: newTarget});
+ }
+ break;
+ case "set":
+ result = new Set();
+ newTarget = {type, value: result};
+ for (let child of value) {
+ queue.push({item: child, target: newTarget});
+ }
+ break;
+ case "object":
+ result = {};
+ newTarget = {type, value: result};
+ for (let [targetName, child] of Object.entries(value)) {
+ queue.push({item: child, target: newTarget, targetName});
+ }
+ break;
+ case "map":
+ result = new Map();
+ newTarget = {type, value: result};
+ for (let [key, child] of value) {
+ queue.push({item: key, target: newTarget, targetName: "key"});
+ queue.push({item: child, target: newTarget, targetName: "value"});
+ }
+ break;
+ default:
+ throw new TypeError(`Can't deserialize object of type ${type}`);
+ }
+ if (objectId !== undefined) {
+ objectMap.set(objectId, result);
+ }
+ }
+
+ if (target === null) {
+ if (deserialized !== null) {
+ throw new Error(`Tried to deserialized a non-root output value without a target`
+ ` container object.`);
+ }
+ deserialized = result;
+ } else {
+ switch(target.type) {
+ case "array":
+ target.value.push(result);
+ break;
+ case "set":
+ target.value.add(result);
+ break;
+ case "object":
+ target.value[targetName] = result;
+ break;
+ case "map":
+ // For maps the same target wrapper is shared between key and value.
+ // After deserializing the key, set the `key` property on the target
+ // until we come to the value.
+ if (targetName === "key") {
+ target.key = result;
+ } else {
+ target.value.set(target.key, result);
+ }
+ break;
+ default:
+ throw new Error(`Unknown target type ${target.type}`);
+ }
+ }
+ }
+ return deserialized;
+ }
+})();
diff --git a/testing/web-platform/tests/resources/check-layout-th.js b/testing/web-platform/tests/resources/check-layout-th.js
new file mode 100644
index 0000000000..f14ca3246b
--- /dev/null
+++ b/testing/web-platform/tests/resources/check-layout-th.js
@@ -0,0 +1,252 @@
+(function() {
+// Test is initiated from body.onload, so explicit done() call is required.
+setup({ explicit_done: true });
+
+function checkSubtreeExpectedValues(t, parent, prefix)
+{
+ var checkedLayout = checkExpectedValues(t, parent, prefix);
+ Array.prototype.forEach.call(parent.childNodes, function(node) {
+ checkedLayout |= checkSubtreeExpectedValues(t, node, prefix);
+ });
+ return checkedLayout;
+}
+
+function checkAttribute(output, node, attribute)
+{
+ var result = node.getAttribute && node.getAttribute(attribute);
+ output.checked |= !!result;
+ return result;
+}
+
+function assert_tolerance(actual, expected, message)
+{
+ if (isNaN(expected) || isNaN(actual) || Math.abs(actual - expected) >= 1) {
+ assert_equals(actual, Number(expected), message);
+ }
+}
+
+function checkDataKeys(node) {
+ // The purpose of this list of data-* attributes is simply to ensure typos
+ // in tests are caught. It is therefore "ok" to add to this list for
+ // specific tests.
+ var validData = new Set([
+ "data-anchor-polyfill",
+ "data-expected-width",
+ "data-expected-height",
+ "data-offset-x",
+ "data-offset-y",
+ "data-expected-client-width",
+ "data-expected-client-height",
+ "data-expected-scroll-width",
+ "data-expected-scroll-height",
+ "data-expected-bounding-client-rect-width",
+ "data-expected-bounding-client-rect-height",
+ "data-total-x",
+ "data-total-y",
+ "data-expected-display",
+ "data-expected-padding-top",
+ "data-expected-padding-bottom",
+ "data-expected-padding-left",
+ "data-expected-padding-right",
+ "data-expected-margin-top",
+ "data-expected-margin-bottom",
+ "data-expected-margin-left",
+ "data-expected-margin-right"
+ ]);
+ if (!node || !node.getAttributeNames)
+ return;
+ // Use "data-test" prefix if you need custom-named data elements.
+ for (let name of node.getAttributeNames()) {
+ if (name.startsWith("data-") && !name.startsWith("data-test"))
+ assert_true(validData.has(name), name + " is a valid data attribute");
+ }
+}
+
+function checkExpectedValues(t, node, prefix)
+{
+ checkDataKeys(node);
+ var output = { checked: false };
+
+ var expectedWidth = checkAttribute(output, node, "data-expected-width");
+ if (expectedWidth) {
+ assert_tolerance(node.offsetWidth, expectedWidth, prefix + "width");
+ }
+
+ var expectedHeight = checkAttribute(output, node, "data-expected-height");
+ if (expectedHeight) {
+ assert_tolerance(node.offsetHeight, expectedHeight, prefix + "height");
+ }
+
+ var expectedOffset = checkAttribute(output, node, "data-offset-x");
+ if (expectedOffset) {
+ assert_tolerance(node.offsetLeft, expectedOffset, prefix + "offsetLeft");
+ }
+
+ var expectedOffset = checkAttribute(output, node, "data-offset-y");
+ if (expectedOffset) {
+ assert_tolerance(node.offsetTop, expectedOffset, prefix + "offsetTop");
+ }
+
+ var expectedWidth = checkAttribute(output, node, "data-expected-client-width");
+ if (expectedWidth) {
+ assert_tolerance(node.clientWidth, expectedWidth, prefix + "clientWidth");
+ }
+
+ var expectedHeight = checkAttribute(output, node, "data-expected-client-height");
+ if (expectedHeight) {
+ assert_tolerance(node.clientHeight, expectedHeight, prefix + "clientHeight");
+ }
+
+ var expectedWidth = checkAttribute(output, node, "data-expected-scroll-width");
+ if (expectedWidth) {
+ assert_tolerance(node.scrollWidth, expectedWidth, prefix + "scrollWidth");
+ }
+
+ var expectedHeight = checkAttribute(output, node, "data-expected-scroll-height");
+ if (expectedHeight) {
+ assert_tolerance(node.scrollHeight, expectedHeight, prefix + "scrollHeight");
+ }
+
+ var expectedWidth = checkAttribute(output, node, "data-expected-bounding-client-rect-width");
+ if (expectedWidth) {
+ assert_tolerance(node.getBoundingClientRect().width, expectedWidth, prefix + "getBoundingClientRect().width");
+ }
+
+ var expectedHeight = checkAttribute(output, node, "data-expected-bounding-client-rect-height");
+ if (expectedHeight) {
+ assert_tolerance(node.getBoundingClientRect().height, expectedHeight, prefix + "getBoundingClientRect().height");
+ }
+
+ var expectedOffset = checkAttribute(output, node, "data-total-x");
+ if (expectedOffset) {
+ var totalLeft = node.clientLeft + node.offsetLeft;
+ assert_tolerance(totalLeft, expectedOffset, prefix +
+ "clientLeft+offsetLeft (" + node.clientLeft + " + " + node.offsetLeft + ")");
+ }
+
+ var expectedOffset = checkAttribute(output, node, "data-total-y");
+ if (expectedOffset) {
+ var totalTop = node.clientTop + node.offsetTop;
+ assert_tolerance(totalTop, expectedOffset, prefix +
+ "clientTop+offsetTop (" + node.clientTop + " + " + node.offsetTop + ")");
+ }
+
+ var expectedDisplay = checkAttribute(output, node, "data-expected-display");
+ if (expectedDisplay) {
+ var actualDisplay = getComputedStyle(node).display;
+ assert_equals(actualDisplay, expectedDisplay, prefix + "display");
+ }
+
+ var expectedPaddingTop = checkAttribute(output, node, "data-expected-padding-top");
+ if (expectedPaddingTop) {
+ var actualPaddingTop = getComputedStyle(node).paddingTop;
+ // Trim the unit "px" from the output.
+ actualPaddingTop = actualPaddingTop.slice(0, -2);
+ assert_equals(actualPaddingTop, expectedPaddingTop, prefix + "padding-top");
+ }
+
+ var expectedPaddingBottom = checkAttribute(output, node, "data-expected-padding-bottom");
+ if (expectedPaddingBottom) {
+ var actualPaddingBottom = getComputedStyle(node).paddingBottom;
+ // Trim the unit "px" from the output.
+ actualPaddingBottom = actualPaddingBottom.slice(0, -2);
+ assert_equals(actualPaddingBottom, expectedPaddingBottom, prefix + "padding-bottom");
+ }
+
+ var expectedPaddingLeft = checkAttribute(output, node, "data-expected-padding-left");
+ if (expectedPaddingLeft) {
+ var actualPaddingLeft = getComputedStyle(node).paddingLeft;
+ // Trim the unit "px" from the output.
+ actualPaddingLeft = actualPaddingLeft.slice(0, -2);
+ assert_equals(actualPaddingLeft, expectedPaddingLeft, prefix + "padding-left");
+ }
+
+ var expectedPaddingRight = checkAttribute(output, node, "data-expected-padding-right");
+ if (expectedPaddingRight) {
+ var actualPaddingRight = getComputedStyle(node).paddingRight;
+ // Trim the unit "px" from the output.
+ actualPaddingRight = actualPaddingRight.slice(0, -2);
+ assert_equals(actualPaddingRight, expectedPaddingRight, prefix + "padding-right");
+ }
+
+ var expectedMarginTop = checkAttribute(output, node, "data-expected-margin-top");
+ if (expectedMarginTop) {
+ var actualMarginTop = getComputedStyle(node).marginTop;
+ // Trim the unit "px" from the output.
+ actualMarginTop = actualMarginTop.slice(0, -2);
+ assert_equals(actualMarginTop, expectedMarginTop, prefix + "margin-top");
+ }
+
+ var expectedMarginBottom = checkAttribute(output, node, "data-expected-margin-bottom");
+ if (expectedMarginBottom) {
+ var actualMarginBottom = getComputedStyle(node).marginBottom;
+ // Trim the unit "px" from the output.
+ actualMarginBottom = actualMarginBottom.slice(0, -2);
+ assert_equals(actualMarginBottom, expectedMarginBottom, prefix + "margin-bottom");
+ }
+
+ var expectedMarginLeft = checkAttribute(output, node, "data-expected-margin-left");
+ if (expectedMarginLeft) {
+ var actualMarginLeft = getComputedStyle(node).marginLeft;
+ // Trim the unit "px" from the output.
+ actualMarginLeft = actualMarginLeft.slice(0, -2);
+ assert_equals(actualMarginLeft, expectedMarginLeft, prefix + "margin-left");
+ }
+
+ var expectedMarginRight = checkAttribute(output, node, "data-expected-margin-right");
+ if (expectedMarginRight) {
+ var actualMarginRight = getComputedStyle(node).marginRight;
+ // Trim the unit "px" from the output.
+ actualMarginRight = actualMarginRight.slice(0, -2);
+ assert_equals(actualMarginRight, expectedMarginRight, prefix + "margin-right");
+ }
+
+ return output.checked;
+}
+
+var testNumber = 0;
+var highlightError = false; // displays outline around failed test element.
+var printDomOnError = true; // prints dom when test fails.
+
+window.checkLayout = function(selectorList, callDone = true)
+{
+ if (!selectorList) {
+ console.error("You must provide a CSS selector of nodes to check.");
+ return;
+ }
+ var nodes = document.querySelectorAll(selectorList);
+ nodes = Array.prototype.slice.call(nodes);
+ var checkedLayout = false;
+ Array.prototype.forEach.call(nodes, function(node) {
+ test(function(t) {
+ var container = node.parentNode.className == 'container' ? node.parentNode : node;
+ var prefix =
+ printDomOnError ? '\n' + container.outerHTML + '\n' : '';
+ var passed = false;
+ try {
+ checkedLayout |= checkExpectedValues(t, node.parentNode, prefix);
+ checkedLayout |= checkSubtreeExpectedValues(t, node, prefix);
+ passed = true;
+ } finally {
+ if (!passed && highlightError) {
+ if (!document.getElementById('testharness_error_css')) {
+ var style = document.createElement('style');
+ style.id = 'testharness_error_css';
+ style.textContent = '.testharness_error { outline: red dotted 2px !important; }';
+ document.body.appendChild(style);
+ }
+ if (node)
+ node.classList.add('testharness_error');
+ }
+ checkedLayout |= !passed;
+ }
+ }, selectorList + ' ' + String(++testNumber));
+ });
+ if (!checkedLayout) {
+ console.error("No valid data-* attributes found in selector list : " + selectorList);
+ }
+ if (callDone)
+ done();
+};
+
+})();
diff --git a/testing/web-platform/tests/resources/check-layout.js b/testing/web-platform/tests/resources/check-layout.js
new file mode 100644
index 0000000000..8634481497
--- /dev/null
+++ b/testing/web-platform/tests/resources/check-layout.js
@@ -0,0 +1,245 @@
+(function() {
+
+function insertAfter(nodeToAdd, referenceNode)
+{
+ if (referenceNode == document.body) {
+ document.body.appendChild(nodeToAdd);
+ return;
+ }
+
+ if (referenceNode.nextSibling)
+ referenceNode.parentNode.insertBefore(nodeToAdd, referenceNode.nextSibling);
+ else
+ referenceNode.parentNode.appendChild(nodeToAdd);
+}
+
+function positionedAncestor(node)
+{
+ var ancestor = node.parentNode;
+ while (getComputedStyle(ancestor).position == 'static')
+ ancestor = ancestor.parentNode;
+ return ancestor;
+}
+
+function checkSubtreeExpectedValues(parent, failures)
+{
+ var checkedLayout = checkExpectedValues(parent, failures);
+ Array.prototype.forEach.call(parent.childNodes, function(node) {
+ checkedLayout |= checkSubtreeExpectedValues(node, failures);
+ });
+ return checkedLayout;
+}
+
+function checkAttribute(output, node, attribute)
+{
+ var result = node.getAttribute && node.getAttribute(attribute);
+ output.checked |= !!result;
+ return result;
+}
+
+function checkExpectedValues(node, failures)
+{
+ var output = { checked: false };
+ var expectedWidth = checkAttribute(output, node, "data-expected-width");
+ if (expectedWidth) {
+ if (isNaN(expectedWidth) || Math.abs(node.offsetWidth - expectedWidth) >= 1)
+ failures.push("Expected " + expectedWidth + " for width, but got " + node.offsetWidth + ". ");
+ }
+
+ var expectedHeight = checkAttribute(output, node, "data-expected-height");
+ if (expectedHeight) {
+ if (isNaN(expectedHeight) || Math.abs(node.offsetHeight - expectedHeight) >= 1)
+ failures.push("Expected " + expectedHeight + " for height, but got " + node.offsetHeight + ". ");
+ }
+
+ var expectedOffset = checkAttribute(output, node, "data-offset-x");
+ if (expectedOffset) {
+ if (isNaN(expectedOffset) || Math.abs(node.offsetLeft - expectedOffset) >= 1)
+ failures.push("Expected " + expectedOffset + " for offsetLeft, but got " + node.offsetLeft + ". ");
+ }
+
+ var expectedOffset = checkAttribute(output, node, "data-offset-y");
+ if (expectedOffset) {
+ if (isNaN(expectedOffset) || Math.abs(node.offsetTop - expectedOffset) >= 1)
+ failures.push("Expected " + expectedOffset + " for offsetTop, but got " + node.offsetTop + ". ");
+ }
+
+ var expectedOffset = checkAttribute(output, node, "data-positioned-offset-x");
+ if (expectedOffset) {
+ var actualOffset = node.getBoundingClientRect().left - positionedAncestor(node).getBoundingClientRect().left;
+ if (isNaN(expectedOffset) || Math.abs(actualOffset - expectedOffset) >= 1)
+ failures.push("Expected " + expectedOffset + " for getBoundingClientRect().left offset, but got " + actualOffset + ". ");
+ }
+
+ var expectedOffset = checkAttribute(output, node, "data-positioned-offset-y");
+ if (expectedOffset) {
+ var actualOffset = node.getBoundingClientRect().top - positionedAncestor(node).getBoundingClientRect().top;
+ if (isNaN(expectedOffset) || Math.abs(actualOffset - expectedOffset) >= 1)
+ failures.push("Expected " + expectedOffset + " for getBoundingClientRect().top offset, but got " + actualOffset + ". ");
+ }
+
+ var expectedWidth = checkAttribute(output, node, "data-expected-client-width");
+ if (expectedWidth) {
+ if (isNaN(expectedWidth) || Math.abs(node.clientWidth - expectedWidth) >= 1)
+ failures.push("Expected " + expectedWidth + " for clientWidth, but got " + node.clientWidth + ". ");
+ }
+
+ var expectedHeight = checkAttribute(output, node, "data-expected-client-height");
+ if (expectedHeight) {
+ if (isNaN(expectedHeight) || Math.abs(node.clientHeight - expectedHeight) >= 1)
+ failures.push("Expected " + expectedHeight + " for clientHeight, but got " + node.clientHeight + ". ");
+ }
+
+ var expectedWidth = checkAttribute(output, node, "data-expected-scroll-width");
+ if (expectedWidth) {
+ if (isNaN(expectedWidth) || Math.abs(node.scrollWidth - expectedWidth) >= 1)
+ failures.push("Expected " + expectedWidth + " for scrollWidth, but got " + node.scrollWidth + ". ");
+ }
+
+ var expectedHeight = checkAttribute(output, node, "data-expected-scroll-height");
+ if (expectedHeight) {
+ if (isNaN(expectedHeight) || Math.abs(node.scrollHeight - expectedHeight) >= 1)
+ failures.push("Expected " + expectedHeight + " for scrollHeight, but got " + node.scrollHeight + ". ");
+ }
+
+ var expectedOffset = checkAttribute(output, node, "data-total-x");
+ if (expectedOffset) {
+ var totalLeft = node.clientLeft + node.offsetLeft;
+ if (isNaN(expectedOffset) || Math.abs(totalLeft - expectedOffset) >= 1)
+ failures.push("Expected " + expectedOffset + " for clientLeft+offsetLeft, but got " + totalLeft + ", clientLeft: " + node.clientLeft + ", offsetLeft: " + node.offsetLeft + ". ");
+ }
+
+ var expectedOffset = checkAttribute(output, node, "data-total-y");
+ if (expectedOffset) {
+ var totalTop = node.clientTop + node.offsetTop;
+ if (isNaN(expectedOffset) || Math.abs(totalTop - expectedOffset) >= 1)
+ failures.push("Expected " + expectedOffset + " for clientTop+offsetTop, but got " + totalTop + ", clientTop: " + node.clientTop + ", + offsetTop: " + node.offsetTop + ". ");
+ }
+
+ var expectedDisplay = checkAttribute(output, node, "data-expected-display");
+ if (expectedDisplay) {
+ var actualDisplay = getComputedStyle(node).display;
+ if (actualDisplay != expectedDisplay)
+ failures.push("Expected " + expectedDisplay + " for display, but got " + actualDisplay + ". ");
+ }
+
+ var expectedPaddingTop = checkAttribute(output, node, "data-expected-padding-top");
+ if (expectedPaddingTop) {
+ var actualPaddingTop = getComputedStyle(node).paddingTop;
+ // Trim the unit "px" from the output.
+ actualPaddingTop = actualPaddingTop.substring(0, actualPaddingTop.length - 2);
+ if (actualPaddingTop != expectedPaddingTop)
+ failures.push("Expected " + expectedPaddingTop + " for padding-top, but got " + actualPaddingTop + ". ");
+ }
+
+ var expectedPaddingBottom = checkAttribute(output, node, "data-expected-padding-bottom");
+ if (expectedPaddingBottom) {
+ var actualPaddingBottom = getComputedStyle(node).paddingBottom;
+ // Trim the unit "px" from the output.
+ actualPaddingBottom = actualPaddingBottom.substring(0, actualPaddingBottom.length - 2);
+ if (actualPaddingBottom != expectedPaddingBottom)
+ failures.push("Expected " + expectedPaddingBottom + " for padding-bottom, but got " + actualPaddingBottom + ". ");
+ }
+
+ var expectedPaddingLeft = checkAttribute(output, node, "data-expected-padding-left");
+ if (expectedPaddingLeft) {
+ var actualPaddingLeft = getComputedStyle(node).paddingLeft;
+ // Trim the unit "px" from the output.
+ actualPaddingLeft = actualPaddingLeft.substring(0, actualPaddingLeft.length - 2);
+ if (actualPaddingLeft != expectedPaddingLeft)
+ failures.push("Expected " + expectedPaddingLeft + " for padding-left, but got " + actualPaddingLeft + ". ");
+ }
+
+ var expectedPaddingRight = checkAttribute(output, node, "data-expected-padding-right");
+ if (expectedPaddingRight) {
+ var actualPaddingRight = getComputedStyle(node).paddingRight;
+ // Trim the unit "px" from the output.
+ actualPaddingRight = actualPaddingRight.substring(0, actualPaddingRight.length - 2);
+ if (actualPaddingRight != expectedPaddingRight)
+ failures.push("Expected " + expectedPaddingRight + " for padding-right, but got " + actualPaddingRight + ". ");
+ }
+
+ var expectedMarginTop = checkAttribute(output, node, "data-expected-margin-top");
+ if (expectedMarginTop) {
+ var actualMarginTop = getComputedStyle(node).marginTop;
+ // Trim the unit "px" from the output.
+ actualMarginTop = actualMarginTop.substring(0, actualMarginTop.length - 2);
+ if (actualMarginTop != expectedMarginTop)
+ failures.push("Expected " + expectedMarginTop + " for margin-top, but got " + actualMarginTop + ". ");
+ }
+
+ var expectedMarginBottom = checkAttribute(output, node, "data-expected-margin-bottom");
+ if (expectedMarginBottom) {
+ var actualMarginBottom = getComputedStyle(node).marginBottom;
+ // Trim the unit "px" from the output.
+ actualMarginBottom = actualMarginBottom.substring(0, actualMarginBottom.length - 2);
+ if (actualMarginBottom != expectedMarginBottom)
+ failures.push("Expected " + expectedMarginBottom + " for margin-bottom, but got " + actualMarginBottom + ". ");
+ }
+
+ var expectedMarginLeft = checkAttribute(output, node, "data-expected-margin-left");
+ if (expectedMarginLeft) {
+ var actualMarginLeft = getComputedStyle(node).marginLeft;
+ // Trim the unit "px" from the output.
+ actualMarginLeft = actualMarginLeft.substring(0, actualMarginLeft.length - 2);
+ if (actualMarginLeft != expectedMarginLeft)
+ failures.push("Expected " + expectedMarginLeft + " for margin-left, but got " + actualMarginLeft + ". ");
+ }
+
+ var expectedMarginRight = checkAttribute(output, node, "data-expected-margin-right");
+ if (expectedMarginRight) {
+ var actualMarginRight = getComputedStyle(node).marginRight;
+ // Trim the unit "px" from the output.
+ actualMarginRight = actualMarginRight.substring(0, actualMarginRight.length - 2);
+ if (actualMarginRight != expectedMarginRight)
+ failures.push("Expected " + expectedMarginRight + " for margin-right, but got " + actualMarginRight + ". ");
+ }
+
+ return output.checked;
+}
+
+window.checkLayout = function(selectorList, outputContainer)
+{
+ var result = true;
+ if (!selectorList) {
+ document.body.appendChild(document.createTextNode("You must provide a CSS selector of nodes to check."));
+ return;
+ }
+ var nodes = document.querySelectorAll(selectorList);
+ nodes = Array.prototype.slice.call(nodes);
+ nodes.reverse();
+ var checkedLayout = false;
+ Array.prototype.forEach.call(nodes, function(node) {
+ var failures = [];
+ checkedLayout |= checkExpectedValues(node.parentNode, failures);
+ checkedLayout |= checkSubtreeExpectedValues(node, failures);
+
+ var container = node.parentNode.className == 'container' ? node.parentNode : node;
+
+ var pre = document.createElement('pre');
+ if (failures.length) {
+ pre.className = 'FAIL';
+ result = false;
+ }
+ pre.appendChild(document.createTextNode(failures.length ? "FAIL:\n" + failures.join('\n') + '\n\n' + container.outerHTML : "PASS"));
+
+ var referenceNode = container;
+ if (outputContainer) {
+ if (!outputContainer.lastChild) {
+ // Inserting a text node so we have something to insertAfter.
+ outputContainer.textContent = " ";
+ }
+ referenceNode = outputContainer.lastChild;
+ }
+ insertAfter(pre, referenceNode);
+ });
+
+ if (!checkedLayout) {
+ document.body.appendChild(document.createTextNode("FAIL: No valid data-* attributes found in selector list : " + selectorList));
+ return false;
+ }
+
+ return result;
+}
+
+})();
diff --git a/testing/web-platform/tests/resources/chromium/README.md b/testing/web-platform/tests/resources/chromium/README.md
new file mode 100644
index 0000000000..be090b332f
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/README.md
@@ -0,0 +1,7 @@
+This directory contains Chromium-specific test resources, including mocks for
+test-only APIs implemented with
+[MojoJS](https://chromium.googlesource.com/chromium/src/+/main/mojo/public/js/README.md).
+
+Please do **not** copy `*.mojom.m.js` into this directory. Follow this doc if you
+want to add new MojoJS-backed mocks:
+https://chromium.googlesource.com/chromium/src/+/main/docs/testing/web_platform_tests.md#mojojs
diff --git a/testing/web-platform/tests/resources/chromium/contacts_manager_mock.js b/testing/web-platform/tests/resources/chromium/contacts_manager_mock.js
new file mode 100644
index 0000000000..049685242b
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/contacts_manager_mock.js
@@ -0,0 +1,90 @@
+// Copyright 2018 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {ContactsManager, ContactsManagerReceiver} from '/gen/third_party/blink/public/mojom/contacts/contacts_manager.mojom.m.js';
+
+self.WebContactsTest = (() => {
+ class MockContacts {
+ constructor() {
+ this.receiver_ = new ContactsManagerReceiver(this);
+
+ this.interceptor_ =
+ new MojoInterfaceInterceptor(ContactsManager.$interfaceName);
+ this.interceptor_.oninterfacerequest =
+ e => this.receiver_.$.bindHandle(e.handle);
+ this.interceptor_.start();
+
+ this.selectedContacts_ = [];
+ }
+
+ formatAddress_(address) {
+ // These are all required fields in the mojo definition.
+ return {
+ country: address.country || '',
+ addressLine: address.addressLine || [],
+ region: address.region || '',
+ city: address.city || '',
+ dependentLocality: address.dependentLocality || '',
+ postalCode: address.postCode || '',
+ sortingCode: address.sortingCode || '',
+ organization: address.organization || '',
+ recipient: address.recipient || '',
+ phone: address.phone || '',
+ };
+ }
+
+ async select(multiple, includeNames, includeEmails, includeTel, includeAddresses, includeIcons) {
+ if (this.selectedContacts_ === null)
+ return {contacts: null};
+
+ const contactInfos = await Promise.all(this.selectedContacts_.map(async contact => {
+ const contactInfo = {};
+ if (includeNames)
+ contactInfo.name = contact.name || [];
+ if (includeEmails)
+ contactInfo.email = contact.email || [];
+ if (includeTel)
+ contactInfo.tel = contact.tel || [];
+ if (includeAddresses) {
+ contactInfo.address = (contact.address || []).map(address => this.formatAddress_(address));
+ }
+ if (includeIcons) {
+ contactInfo.icon = await Promise.all(
+ (contact.icon || []).map(async blob => ({
+ mimeType: blob.type,
+ data: (await blob.text()).split('').map(s => s.charCodeAt(0)),
+ })));
+ }
+ return contactInfo;
+ }));
+
+ if (!contactInfos.length) return {contacts: []};
+ if (!multiple) return {contacts: [contactInfos[0]]};
+ return {contacts: contactInfos};
+ }
+
+ setSelectedContacts(contacts) {
+ this.selectedContacts_ = contacts;
+ }
+
+ reset() {
+ this.receiver_.$.close();
+ this.interceptor_.stop();
+ }
+ }
+
+ const mockContacts = new MockContacts();
+
+ class ContactsTestChromium {
+ constructor() {
+ Object.freeze(this); // Make it immutable.
+ }
+
+ setSelectedContacts(contacts) {
+ mockContacts.setSelectedContacts(contacts);
+ }
+ }
+
+ return ContactsTestChromium;
+})();
diff --git a/testing/web-platform/tests/resources/chromium/content-index-helpers.js b/testing/web-platform/tests/resources/chromium/content-index-helpers.js
new file mode 100644
index 0000000000..936fe84c9b
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/content-index-helpers.js
@@ -0,0 +1,9 @@
+import {ContentIndexService} from '/gen/third_party/blink/public/mojom/content_index/content_index.mojom.m.js';
+
+// Returns a promise if the chromium based browser fetches icons for
+// content-index.
+export async function fetchesIcons() {
+ const remote = ContentIndexService.getRemote();
+ const {iconSizes} = await remote.getIconSizes();
+ return iconSizes.length > 0;
+};
diff --git a/testing/web-platform/tests/resources/chromium/enable-hyperlink-auditing.js b/testing/web-platform/tests/resources/chromium/enable-hyperlink-auditing.js
new file mode 100644
index 0000000000..263f6512f0
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/enable-hyperlink-auditing.js
@@ -0,0 +1,2 @@
+if (window.testRunner)
+ testRunner.overridePreference("WebKitHyperlinkAuditingEnabled", 1);
diff --git a/testing/web-platform/tests/resources/chromium/fake-hid.js b/testing/web-platform/tests/resources/chromium/fake-hid.js
new file mode 100644
index 0000000000..70a01490d8
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/fake-hid.js
@@ -0,0 +1,297 @@
+import {HidConnectionReceiver, HidDeviceInfo} from '/gen/services/device/public/mojom/hid.mojom.m.js';
+import {HidService, HidServiceReceiver} from '/gen/third_party/blink/public/mojom/hid/hid.mojom.m.js';
+
+// Fake implementation of device.mojom.HidConnection. HidConnection represents
+// an open connection to a HID device and can be used to send and receive
+// reports.
+class FakeHidConnection {
+ constructor(client) {
+ this.client_ = client;
+ this.receiver_ = new HidConnectionReceiver(this);
+ this.expectedWrites_ = [];
+ this.expectedGetFeatureReports_ = [];
+ this.expectedSendFeatureReports_ = [];
+ }
+
+ bindNewPipeAndPassRemote() {
+ return this.receiver_.$.bindNewPipeAndPassRemote();
+ }
+
+ // Simulate an input report sent from the device to the host. The connection
+ // client's onInputReport method will be called with the provided |reportId|
+ // and |buffer|.
+ simulateInputReport(reportId, reportData) {
+ if (this.client_) {
+ this.client_.onInputReport(reportId, reportData);
+ }
+ }
+
+ // Specify the result for an expected call to write. If |success| is true the
+ // write will be successful, otherwise it will simulate a failure. The
+ // parameters of the next write call must match |reportId| and |buffer|.
+ queueExpectedWrite(success, reportId, reportData) {
+ this.expectedWrites_.push({
+ params: {reportId, data: reportData},
+ result: {success},
+ });
+ }
+
+ // Specify the result for an expected call to getFeatureReport. If |success|
+ // is true the operation is successful, otherwise it will simulate a failure.
+ // The parameter of the next getFeatureReport call must match |reportId|.
+ queueExpectedGetFeatureReport(success, reportId, reportData) {
+ this.expectedGetFeatureReports_.push({
+ params: {reportId},
+ result: {success, buffer: reportData},
+ });
+ }
+
+ // Specify the result for an expected call to sendFeatureReport. If |success|
+ // is true the operation is successful, otherwise it will simulate a failure.
+ // The parameters of the next sendFeatureReport call must match |reportId| and
+ // |buffer|.
+ queueExpectedSendFeatureReport(success, reportId, reportData) {
+ this.expectedSendFeatureReports_.push({
+ params: {reportId, data: reportData},
+ result: {success},
+ });
+ }
+
+ // Asserts that there are no more expected operations.
+ assertExpectationsMet() {
+ assert_equals(this.expectedWrites_.length, 0);
+ assert_equals(this.expectedGetFeatureReports_.length, 0);
+ assert_equals(this.expectedSendFeatureReports_.length, 0);
+ }
+
+ read() {}
+
+ // Implementation of HidConnection::Write. Causes an assertion failure if
+ // there are no expected write operations, or if the parameters do not match
+ // the expected call.
+ async write(reportId, buffer) {
+ let expectedWrite = this.expectedWrites_.shift();
+ assert_not_equals(expectedWrite, undefined);
+ assert_equals(reportId, expectedWrite.params.reportId);
+ let actual = new Uint8Array(buffer);
+ compareDataViews(
+ new DataView(actual.buffer, actual.byteOffset),
+ new DataView(
+ expectedWrite.params.data.buffer,
+ expectedWrite.params.data.byteOffset));
+ return expectedWrite.result;
+ }
+
+ // Implementation of HidConnection::GetFeatureReport. Causes an assertion
+ // failure if there are no expected write operations, or if the parameters do
+ // not match the expected call.
+ async getFeatureReport(reportId) {
+ let expectedGetFeatureReport = this.expectedGetFeatureReports_.shift();
+ assert_not_equals(expectedGetFeatureReport, undefined);
+ assert_equals(reportId, expectedGetFeatureReport.params.reportId);
+ return expectedGetFeatureReport.result;
+ }
+
+ // Implementation of HidConnection::SendFeatureReport. Causes an assertion
+ // failure if there are no expected write operations, or if the parameters do
+ // not match the expected call.
+ async sendFeatureReport(reportId, buffer) {
+ let expectedSendFeatureReport = this.expectedSendFeatureReports_.shift();
+ assert_not_equals(expectedSendFeatureReport, undefined);
+ assert_equals(reportId, expectedSendFeatureReport.params.reportId);
+ let actual = new Uint8Array(buffer);
+ compareDataViews(
+ new DataView(actual.buffer, actual.byteOffset),
+ new DataView(
+ expectedSendFeatureReport.params.data.buffer,
+ expectedSendFeatureReport.params.data.byteOffset));
+ return expectedSendFeatureReport.result;
+ }
+}
+
+
+// A fake implementation of the HidService mojo interface. HidService manages
+// HID device access for clients in the render process. Typically, when a client
+// requests access to a HID device a chooser dialog is shown with a list of
+// available HID devices. Selecting a device from the chooser also grants
+// permission for the client to access that device.
+//
+// The fake implementation allows tests to simulate connected devices. It also
+// skips the chooser dialog and instead allows tests to specify which device
+// should be selected. All devices are treated as if the user had already
+// granted permission. It is possible to revoke permission with forget() later.
+class FakeHidService {
+ constructor() {
+ this.interceptor_ = new MojoInterfaceInterceptor(HidService.$interfaceName);
+ this.interceptor_.oninterfacerequest = e => this.bind(e.handle);
+ this.receiver_ = new HidServiceReceiver(this);
+ this.nextGuidValue_ = 0;
+ this.simulateConnectFailure_ = false;
+ this.reset();
+ }
+
+ start() {
+ this.interceptor_.start();
+ }
+
+ stop() {
+ this.interceptor_.stop();
+ }
+
+ reset() {
+ this.devices_ = new Map();
+ this.allowedDevices_ = new Map();
+ this.fakeConnections_ = new Map();
+ this.selectedDevices_ = [];
+ }
+
+ // Creates and returns a HidDeviceInfo with the specified device IDs.
+ makeDevice(vendorId, productId) {
+ let guidValue = ++this.nextGuidValue_;
+ let info = new HidDeviceInfo();
+ info.guid = 'guid-' + guidValue.toString();
+ info.physicalDeviceId = 'physical-device-id-' + guidValue.toString();
+ info.vendorId = vendorId;
+ info.productId = productId;
+ info.productName = 'product name';
+ info.serialNumber = '0';
+ info.reportDescriptor = new Uint8Array();
+ info.collections = [];
+ info.deviceNode = 'device node';
+ return info;
+ }
+
+ // Simulates a connected device the client has already been granted permission
+ // to. Returns the key used to store the device in the map. The key is either
+ // the physical device ID, or the device GUID if it has no physical device ID.
+ addDevice(deviceInfo, grantPermission = true) {
+ let key = deviceInfo.physicalDeviceId;
+ if (key.length === 0)
+ key = deviceInfo.guid;
+
+ let devices = this.devices_.get(key) || [];
+ devices.push(deviceInfo);
+ this.devices_.set(key, devices);
+
+ if (grantPermission) {
+ let allowedDevices = this.allowedDevices_.get(key) || [];
+ allowedDevices.push(deviceInfo);
+ this.allowedDevices_.set(key, allowedDevices);
+ }
+
+ if (this.client_)
+ this.client_.deviceAdded(deviceInfo);
+ return key;
+ }
+
+ // Simulates disconnecting a connected device.
+ removeDevice(key) {
+ let devices = this.devices_.get(key);
+ this.devices_.delete(key);
+ if (this.client_ && devices) {
+ devices.forEach(deviceInfo => {
+ this.client_.deviceRemoved(deviceInfo);
+ });
+ }
+ }
+
+ // Simulates updating the device information for a connected device.
+ changeDevice(deviceInfo) {
+ let key = deviceInfo.physicalDeviceId;
+ if (key.length === 0)
+ key = deviceInfo.guid;
+
+ let devices = this.devices_.get(key) || [];
+ let i = devices.length;
+ while (i--) {
+ if (devices[i].guid == deviceInfo.guid)
+ devices.splice(i, 1);
+ }
+ devices.push(deviceInfo);
+ this.devices_.set(key, devices);
+
+ let allowedDevices = this.allowedDevices_.get(key) || [];
+ let j = allowedDevices.length;
+ while (j--) {
+ if (allowedDevices[j].guid == deviceInfo.guid)
+ allowedDevices.splice(j, 1);
+ }
+ allowedDevices.push(deviceInfo);
+ this.allowedDevices_.set(key, allowedDevices);
+
+ if (this.client_)
+ this.client_.deviceChanged(deviceInfo);
+ return key;
+ }
+
+ // Sets a flag that causes the next call to connect() to fail.
+ simulateConnectFailure() {
+ this.simulateConnectFailure_ = true;
+ }
+
+ // Sets the key of the device that will be returned as the selected item the
+ // next time requestDevice is called. The device with this key must have been
+ // previously added with addDevice.
+ setSelectedDevice(key) {
+ this.selectedDevices_ = this.devices_.get(key);
+ }
+
+ // Returns the fake HidConnection object for this device, if there is one. A
+ // connection is created once the device is opened.
+ getFakeConnection(guid) {
+ return this.fakeConnections_.get(guid);
+ }
+
+ bind(handle) {
+ this.receiver_.$.bindHandle(handle);
+ }
+
+ registerClient(client) {
+ this.client_ = client;
+ }
+
+ // Returns an array of connected devices the client has already been granted
+ // permission to access.
+ async getDevices() {
+ let devices = [];
+ this.allowedDevices_.forEach((value) => {
+ devices = devices.concat(value);
+ });
+ return {devices};
+ }
+
+ // Simulates a device chooser prompt, returning |selectedDevices_| as the
+ // simulated selection. |options| is ignored.
+ async requestDevice(options) {
+ return {devices: this.selectedDevices_};
+ }
+
+ // Returns a fake connection to the device with the specified GUID. If
+ // |connectionClient| is not null, its onInputReport method will be called
+ // when input reports are received. If simulateConnectFailure() was called
+ // then a null connection is returned instead, indicating failure.
+ async connect(guid, connectionClient) {
+ if (this.simulateConnectFailure_) {
+ this.simulateConnectFailure_ = false;
+ return {connection: null};
+ }
+ const fakeConnection = new FakeHidConnection(connectionClient);
+ this.fakeConnections_.set(guid, fakeConnection);
+ return {connection: fakeConnection.bindNewPipeAndPassRemote()};
+ }
+
+ // Removes the allowed device.
+ async forget(deviceInfo) {
+ for (const [key, value] of this.allowedDevices_) {
+ for (const device of value) {
+ if (device.guid == deviceInfo.guid) {
+ this.allowedDevices_.delete(key);
+ break;
+ }
+ }
+ }
+ return {success: true};
+ }
+}
+
+export const fakeHidService = new FakeHidService();
diff --git a/testing/web-platform/tests/resources/chromium/fake-serial.js b/testing/web-platform/tests/resources/chromium/fake-serial.js
new file mode 100644
index 0000000000..e1e4d57e3e
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/fake-serial.js
@@ -0,0 +1,443 @@
+import {SerialPortFlushMode, SerialPortRemote, SerialReceiveError, SerialPortReceiver, SerialSendError} from '/gen/services/device/public/mojom/serial.mojom.m.js';
+import {SerialService, SerialServiceReceiver} from '/gen/third_party/blink/public/mojom/serial/serial.mojom.m.js';
+
+// Implementation of an UnderlyingSource to create a ReadableStream from a Mojo
+// data pipe consumer handle.
+class DataPipeSource {
+ constructor(consumer) {
+ this.consumer_ = consumer;
+ }
+
+ async pull(controller) {
+ let chunk = new ArrayBuffer(64);
+ let {result, numBytes} = this.consumer_.readData(chunk);
+ if (result == Mojo.RESULT_OK) {
+ controller.enqueue(new Uint8Array(chunk, 0, numBytes));
+ return;
+ } else if (result == Mojo.RESULT_FAILED_PRECONDITION) {
+ controller.close();
+ return;
+ } else if (result == Mojo.RESULT_SHOULD_WAIT) {
+ await this.readable();
+ return this.pull(controller);
+ }
+ }
+
+ cancel() {
+ if (this.watcher_)
+ this.watcher_.cancel();
+ this.consumer_.close();
+ }
+
+ readable() {
+ return new Promise((resolve) => {
+ this.watcher_ =
+ this.consumer_.watch({ readable: true, peerClosed: true }, () => {
+ this.watcher_.cancel();
+ this.watcher_ = undefined;
+ resolve();
+ });
+ });
+ }
+}
+
+// Implementation of an UnderlyingSink to create a WritableStream from a Mojo
+// data pipe producer handle.
+class DataPipeSink {
+ constructor(producer) {
+ this._producer = producer;
+ }
+
+ async write(chunk, controller) {
+ while (true) {
+ let {result, numBytes} = this._producer.writeData(chunk);
+ if (result == Mojo.RESULT_OK) {
+ if (numBytes == chunk.byteLength) {
+ return;
+ }
+ chunk = chunk.slice(numBytes);
+ } else if (result == Mojo.RESULT_FAILED_PRECONDITION) {
+ throw new DOMException('The pipe is closed.', 'InvalidStateError');
+ } else if (result == Mojo.RESULT_SHOULD_WAIT) {
+ await this.writable();
+ }
+ }
+ }
+
+ close() {
+ assert_equals(undefined, this._watcher);
+ this._producer.close();
+ }
+
+ abort(reason) {
+ if (this._watcher)
+ this._watcher.cancel();
+ this._producer.close();
+ }
+
+ writable() {
+ return new Promise((resolve) => {
+ this._watcher =
+ this._producer.watch({ writable: true, peerClosed: true }, () => {
+ this._watcher.cancel();
+ this._watcher = undefined;
+ resolve();
+ });
+ });
+ }
+}
+
+// Implementation of device.mojom.SerialPort.
+class FakeSerialPort {
+ constructor() {
+ this.inputSignals_ = {
+ dataCarrierDetect: false,
+ clearToSend: false,
+ ringIndicator: false,
+ dataSetReady: false
+ };
+ this.inputSignalFailure_ = false;
+ this.outputSignals_ = {
+ dataTerminalReady: false,
+ requestToSend: false,
+ break: false
+ };
+ this.outputSignalFailure_ = false;
+ }
+
+ open(options, client) {
+ if (this.receiver_ !== undefined) {
+ // Port already open.
+ return null;
+ }
+
+ let port = new SerialPortRemote();
+ this.receiver_ = new SerialPortReceiver(this);
+ this.receiver_.$.bindHandle(port.$.bindNewPipeAndPassReceiver().handle);
+
+ this.options_ = options;
+ this.client_ = client;
+ // OS typically sets DTR on open.
+ this.outputSignals_.dataTerminalReady = true;
+
+ return port;
+ }
+
+ write(data) {
+ return this.writer_.write(data);
+ }
+
+ read() {
+ return this.reader_.read();
+ }
+
+ // Reads from the port until at least |targetLength| is read or the stream is
+ // closed. The data is returned as a combined Uint8Array.
+ readWithLength(targetLength) {
+ return readWithLength(this.reader_, targetLength);
+ }
+
+ simulateReadError(error) {
+ this.writer_.close();
+ this.writer_.releaseLock();
+ this.writer_ = undefined;
+ this.writable_ = undefined;
+ this.client_.onReadError(error);
+ }
+
+ simulateParityError() {
+ this.simulateReadError(SerialReceiveError.PARITY_ERROR);
+ }
+
+ simulateDisconnectOnRead() {
+ this.simulateReadError(SerialReceiveError.DISCONNECTED);
+ }
+
+ simulateWriteError(error) {
+ this.reader_.cancel();
+ this.reader_ = undefined;
+ this.readable_ = undefined;
+ this.client_.onSendError(error);
+ }
+
+ simulateSystemErrorOnWrite() {
+ this.simulateWriteError(SerialSendError.SYSTEM_ERROR);
+ }
+
+ simulateDisconnectOnWrite() {
+ this.simulateWriteError(SerialSendError.DISCONNECTED);
+ }
+
+ simulateInputSignals(signals) {
+ this.inputSignals_ = signals;
+ }
+
+ simulateInputSignalFailure(fail) {
+ this.inputSignalFailure_ = fail;
+ }
+
+ get outputSignals() {
+ return this.outputSignals_;
+ }
+
+ simulateOutputSignalFailure(fail) {
+ this.outputSignalFailure_ = fail;
+ }
+
+ writable() {
+ if (this.writable_)
+ return Promise.resolve();
+
+ if (!this.writablePromise_) {
+ this.writablePromise_ = new Promise((resolve) => {
+ this.writableResolver_ = resolve;
+ });
+ }
+
+ return this.writablePromise_;
+ }
+
+ readable() {
+ if (this.readable_)
+ return Promise.resolve();
+
+ if (!this.readablePromise_) {
+ this.readablePromise_ = new Promise((resolve) => {
+ this.readableResolver_ = resolve;
+ });
+ }
+
+ return this.readablePromise_;
+ }
+
+ async startWriting(in_stream) {
+ this.readable_ = new ReadableStream(new DataPipeSource(in_stream));
+ this.reader_ = this.readable_.getReader();
+ if (this.readableResolver_) {
+ this.readableResolver_();
+ this.readableResolver_ = undefined;
+ this.readablePromise_ = undefined;
+ }
+ }
+
+ async startReading(out_stream) {
+ this.writable_ = new WritableStream(new DataPipeSink(out_stream));
+ this.writer_ = this.writable_.getWriter();
+ if (this.writableResolver_) {
+ this.writableResolver_();
+ this.writableResolver_ = undefined;
+ this.writablePromise_ = undefined;
+ }
+ }
+
+ async flush(mode) {
+ switch (mode) {
+ case SerialPortFlushMode.kReceive:
+ this.writer_.abort();
+ this.writer_.releaseLock();
+ this.writer_ = undefined;
+ this.writable_ = undefined;
+ break;
+ case SerialPortFlushMode.kTransmit:
+ this.reader_.cancel();
+ this.reader_ = undefined;
+ this.readable_ = undefined;
+ break;
+ }
+ }
+
+ async drain() {
+ await this.reader_.closed;
+ }
+
+ async getControlSignals() {
+ if (this.inputSignalFailure_) {
+ return {signals: null};
+ }
+
+ const signals = {
+ dcd: this.inputSignals_.dataCarrierDetect,
+ cts: this.inputSignals_.clearToSend,
+ ri: this.inputSignals_.ringIndicator,
+ dsr: this.inputSignals_.dataSetReady
+ };
+ return {signals};
+ }
+
+ async setControlSignals(signals) {
+ if (this.outputSignalFailure_) {
+ return {success: false};
+ }
+
+ if (signals.hasDtr) {
+ this.outputSignals_.dataTerminalReady = signals.dtr;
+ }
+ if (signals.hasRts) {
+ this.outputSignals_.requestToSend = signals.rts;
+ }
+ if (signals.hasBrk) {
+ this.outputSignals_.break = signals.brk;
+ }
+ return { success: true };
+ }
+
+ async configurePort(options) {
+ this.options_ = options;
+ return { success: true };
+ }
+
+ async getPortInfo() {
+ return {
+ bitrate: this.options_.bitrate,
+ dataBits: this.options_.datBits,
+ parityBit: this.options_.parityBit,
+ stopBits: this.options_.stopBits,
+ ctsFlowControl:
+ this.options_.hasCtsFlowControl && this.options_.ctsFlowControl,
+ };
+ }
+
+ async close() {
+ // OS typically clears DTR on close.
+ this.outputSignals_.dataTerminalReady = false;
+ if (this.writer_) {
+ this.writer_.close();
+ this.writer_.releaseLock();
+ this.writer_ = undefined;
+ }
+ this.writable_ = undefined;
+
+ // Close the receiver asynchronously so the reply to this message can be
+ // sent first.
+ const receiver = this.receiver_;
+ this.receiver_ = undefined;
+ setTimeout(() => {
+ receiver.$.close();
+ }, 0);
+
+ return {};
+ }
+}
+
+// Implementation of blink.mojom.SerialService.
+class FakeSerialService {
+ constructor() {
+ this.interceptor_ =
+ new MojoInterfaceInterceptor(SerialService.$interfaceName);
+ this.interceptor_.oninterfacerequest = e => this.bind(e.handle);
+ this.receiver_ = new SerialServiceReceiver(this);
+ this.clients_ = [];
+ this.nextToken_ = 0;
+ this.reset();
+ }
+
+ start() {
+ this.interceptor_.start();
+ }
+
+ stop() {
+ this.interceptor_.stop();
+ }
+
+ reset() {
+ this.ports_ = new Map();
+ this.selectedPort_ = null;
+ }
+
+ addPort(info) {
+ let portInfo = {};
+ if (info?.usbVendorId !== undefined) {
+ portInfo.hasUsbVendorId = true;
+ portInfo.usbVendorId = info.usbVendorId;
+ }
+ if (info?.usbProductId !== undefined) {
+ portInfo.hasUsbProductId = true;
+ portInfo.usbProductId = info.usbProductId;
+ }
+
+ let token = ++this.nextToken_;
+ portInfo.token = {high: 0n, low: BigInt(token)};
+
+ let record = {
+ portInfo: portInfo,
+ fakePort: new FakeSerialPort(),
+ };
+ this.ports_.set(token, record);
+
+ for (let client of this.clients_) {
+ client.onPortAdded(portInfo);
+ }
+
+ return token;
+ }
+
+ removePort(token) {
+ let record = this.ports_.get(token);
+ if (record === undefined) {
+ return;
+ }
+
+ this.ports_.delete(token);
+
+ for (let client of this.clients_) {
+ client.onPortRemoved(record.portInfo);
+ }
+ }
+
+ setSelectedPort(token) {
+ this.selectedPort_ = this.ports_.get(token);
+ }
+
+ getFakePort(token) {
+ let record = this.ports_.get(token);
+ if (record === undefined)
+ return undefined;
+ return record.fakePort;
+ }
+
+ bind(handle) {
+ this.receiver_.$.bindHandle(handle);
+ }
+
+ async setClient(client_remote) {
+ this.clients_.push(client_remote);
+ }
+
+ async getPorts() {
+ return {
+ ports: Array.from(this.ports_, ([token, record]) => record.portInfo)
+ };
+ }
+
+ async requestPort(filters) {
+ if (this.selectedPort_)
+ return { port: this.selectedPort_.portInfo };
+ else
+ return { port: null };
+ }
+
+ async openPort(token, options, client) {
+ let record = this.ports_.get(Number(token.low));
+ if (record !== undefined) {
+ return {port: record.fakePort.open(options, client)};
+ } else {
+ return {port: null};
+ }
+ }
+
+ async forgetPort(token) {
+ let record = this.ports_.get(Number(token.low));
+ if (record === undefined) {
+ return {success: false};
+ }
+
+ this.ports_.delete(Number(token.low));
+ if (record.fakePort.receiver_) {
+ record.fakePort.receiver_.$.close();
+ record.fakePort.receiver_ = undefined;
+ }
+ return {success: true};
+ }
+}
+
+export const fakeSerialService = new FakeSerialService();
diff --git a/testing/web-platform/tests/resources/chromium/generic_sensor_mocks.js b/testing/web-platform/tests/resources/chromium/generic_sensor_mocks.js
new file mode 100644
index 0000000000..98a29c2104
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/generic_sensor_mocks.js
@@ -0,0 +1,519 @@
+import {ReportingMode, Sensor, SensorClientRemote, SensorReceiver, SensorRemote, SensorType} from '/gen/services/device/public/mojom/sensor.mojom.m.js';
+import {SensorCreationResult, SensorInitParams_READ_BUFFER_SIZE_FOR_TESTS, SensorProvider, SensorProviderReceiver} from '/gen/services/device/public/mojom/sensor_provider.mojom.m.js';
+
+// A "sliding window" that iterates over |data| and returns one item at a
+// time, advancing and wrapping around as needed. |data| must be an array of
+// arrays.
+self.RingBuffer = class {
+ constructor(data) {
+ this.bufferPosition_ = 0;
+ // Validate |data|'s format and deep-copy every element.
+ this.data_ = Array.from(data, element => {
+ if (!Array.isArray(element)) {
+ throw new TypeError('Every |data| element must be an array.');
+ }
+ return Array.from(element);
+ })
+ }
+
+ next() {
+ const value = this.data_[this.bufferPosition_];
+ this.bufferPosition_ = (this.bufferPosition_ + 1) % this.data_.length;
+ return { done: false, value: value };
+ }
+
+ value() {
+ return this.data_[this.bufferPosition_];
+ }
+
+ [Symbol.iterator]() {
+ return this;
+ }
+};
+
+class DefaultSensorTraits {
+ // https://w3c.github.io/sensors/#threshold-check-algorithm
+ static isSignificantlyDifferent(reading1, reading2) {
+ return true;
+ }
+
+ // https://w3c.github.io/sensors/#reading-quantization-algorithm
+ static roundToMultiple(reading) {
+ return reading;
+ }
+
+ // https://w3c.github.io/ambient-light/#ambient-light-threshold-check-algorithm
+ static areReadingsEqual(reading1, reading2) {
+ return false;
+ }
+}
+
+class AmbientLightSensorTraits extends DefaultSensorTraits {
+ // https://w3c.github.io/ambient-light/#reduce-sensor-accuracy
+ static #ROUNDING_MULTIPLE = 50;
+ static #SIGNIFICANCE_THRESHOLD = 25;
+
+ // https://w3c.github.io/ambient-light/#ambient-light-threshold-check-algorithm
+ static isSignificantlyDifferent([illuminance1], [illuminance2]) {
+ return Math.abs(illuminance1 - illuminance2) >=
+ this.#SIGNIFICANCE_THRESHOLD;
+ }
+
+ // https://w3c.github.io/ambient-light/#ambient-light-reading-quantization-algorithm
+ static roundToMultiple(reading) {
+ const illuminance = reading[0];
+ const scaledValue =
+ illuminance / AmbientLightSensorTraits.#ROUNDING_MULTIPLE;
+ let roundedReading = reading.splice();
+
+ if (illuminance < 0.0) {
+ roundedReading[0] = -AmbientLightSensorTraits.#ROUNDING_MULTIPLE *
+ Math.floor(-scaledValue + 0.5);
+ } else {
+ roundedReading[0] = AmbientLightSensorTraits.#ROUNDING_MULTIPLE *
+ Math.floor(scaledValue + 0.5);
+ }
+
+ return roundedReading;
+ }
+
+ // https://w3c.github.io/ambient-light/#ambient-light-threshold-check-algorithm
+ static areReadingsEqual([illuminance1], [illuminance2]) {
+ return illuminance1 === illuminance2;
+ }
+}
+
+self.GenericSensorTest = (() => {
+ // Default sensor frequency in default configurations.
+ const DEFAULT_FREQUENCY = 5;
+
+ // Class that mocks Sensor interface defined in
+ // https://cs.chromium.org/chromium/src/services/device/public/mojom/sensor.mojom
+ class MockSensor {
+ static #BUFFER_OFFSET_TIMESTAMP = 1;
+ static #BUFFER_OFFSET_READINGS = 2;
+
+ constructor(sensorRequest, buffer, reportingMode, sensorType) {
+ this.client_ = null;
+ this.startShouldFail_ = false;
+ this.notifyOnReadingChange_ = true;
+ this.reportingMode_ = reportingMode;
+ this.sensorType_ = sensorType;
+ this.sensorReadingTimerId_ = null;
+ this.readingData_ = null;
+ this.requestedFrequencies_ = [];
+ // The Blink implementation (third_party/blink/renderer/modules/sensor/sensor.cc)
+ // sets a timestamp by creating a DOMHighResTimeStamp from a given platform timestamp.
+ // In this mock implementation we use a starting value
+ // and an increment step value that resemble a platform timestamp reasonably enough.
+ this.timestamp_ = window.performance.timeOrigin;
+ // |buffer| represents a SensorReadingSharedBuffer on the C++ side in
+ // Chromium. It consists, in this order, of a
+ // SensorReadingField<OneWriterSeqLock> (an 8-byte union that includes
+ // 32-bit integer used by the lock class), and a SensorReading consisting
+ // of an 8-byte timestamp and 4 8-byte reading fields.
+ //
+ // |this.buffer_[0]| is zeroed by default, which allows OneWriterSeqLock
+ // to work with our custom memory buffer that did not actually create a
+ // OneWriterSeqLock instance. It is never changed manually here.
+ //
+ // Use MockSensor.#BUFFER_OFFSET_TIMESTAMP and
+ // MockSensor.#BUFFER_OFFSET_READINGS to access the other positions in
+ // |this.buffer_| without having to hardcode magic numbers in the code.
+ this.buffer_ = buffer;
+ this.buffer_.fill(0);
+ this.receiver_ = new SensorReceiver(this);
+ this.receiver_.$.bindHandle(sensorRequest.handle);
+ this.lastRawReading_ = null;
+ this.lastRoundedReading_ = null;
+
+ if (sensorType == SensorType.AMBIENT_LIGHT) {
+ this.sensorTraits = AmbientLightSensorTraits;
+ } else {
+ this.sensorTraits = DefaultSensorTraits;
+ }
+ }
+
+ // Returns default configuration.
+ async getDefaultConfiguration() {
+ return { frequency: DEFAULT_FREQUENCY };
+ }
+
+ // Adds configuration for the sensor and starts reporting fake data
+ // through setSensorReading function.
+ async addConfiguration(configuration) {
+ this.requestedFrequencies_.push(configuration.frequency);
+ // Sort using descending order.
+ this.requestedFrequencies_.sort(
+ (first, second) => { return second - first });
+
+ if (!this.startShouldFail_ )
+ this.startReading();
+
+ return { success: !this.startShouldFail_ };
+ }
+
+ // Removes sensor configuration from the list of active configurations and
+ // stops notification about sensor reading changes if
+ // requestedFrequencies_ is empty.
+ removeConfiguration(configuration) {
+ const index = this.requestedFrequencies_.indexOf(configuration.frequency);
+ if (index == -1)
+ return;
+
+ this.requestedFrequencies_.splice(index, 1);
+ if (this.requestedFrequencies_.length === 0)
+ this.stopReading();
+ }
+
+ // ConfigureReadingChangeNotifications(bool enabled)
+ // Configures whether to report a reading change when in ON_CHANGE
+ // reporting mode.
+ configureReadingChangeNotifications(notifyOnReadingChange) {
+ this.notifyOnReadingChange_ = notifyOnReadingChange;
+ }
+
+ resume() {
+ this.startReading();
+ }
+
+ suspend() {
+ this.stopReading();
+ }
+
+ // Mock functions
+
+ // Resets mock Sensor state.
+ reset() {
+ this.stopReading();
+ this.startShouldFail_ = false;
+ this.requestedFrequencies_ = [];
+ this.notifyOnReadingChange_ = true;
+ this.readingData_ = null;
+ this.buffer_.fill(0);
+ this.receiver_.$.close();
+ this.lastRawReading_ = null;
+ this.lastRoundedReading_ = null;
+ }
+
+ // Sets fake data that is used to deliver sensor reading updates.
+ setSensorReading(readingData) {
+ this.readingData_ = new RingBuffer(readingData);
+ }
+
+ // This is a workaround to accommodate Blink's Device Orientation
+ // implementation. In general, all tests should use setSensorReading()
+ // instead.
+ setSensorReadingImmediately(readingData) {
+ this.setSensorReading(readingData);
+
+ const reading = this.readingData_.value();
+ this.buffer_.set(reading, MockSensor.#BUFFER_OFFSET_READINGS);
+ this.buffer_[MockSensor.#BUFFER_OFFSET_TIMESTAMP] = this.timestamp_++;
+ }
+
+ // Sets flag that forces sensor to fail when addConfiguration is invoked.
+ setStartShouldFail(shouldFail) {
+ this.startShouldFail_ = shouldFail;
+ }
+
+ startReading() {
+ if (this.readingData_ != null) {
+ this.stopReading();
+ }
+ let maxFrequencyUsed = this.requestedFrequencies_[0];
+ let timeout = (1 / maxFrequencyUsed) * 1000;
+ this.sensorReadingTimerId_ = window.setInterval(() => {
+ if (this.readingData_) {
+ // |buffer_| is a TypedArray, so we need to make sure pass an
+ // array to set().
+ const reading = this.readingData_.next().value;
+ if (!Array.isArray(reading)) {
+ throw new TypeError("startReading(): The readings passed to " +
+ "setSensorReading() must be arrays");
+ }
+
+ if (this.reportingMode_ == ReportingMode.ON_CHANGE &&
+ this.lastRawReading_ !== null &&
+ !this.sensorTraits.isSignificantlyDifferent(
+ this.lastRawReading_, reading)) {
+ // In case new value is not significantly different compared to
+ // old value, new value is not sent.
+ return;
+ }
+
+ this.lastRawReading_ = reading.slice();
+ const roundedReading = this.sensorTraits.roundToMultiple(reading);
+
+ if (this.reportingMode_ == ReportingMode.ON_CHANGE &&
+ this.lastRoundedReading_ !== null &&
+ this.sensorTraits.areReadingsEqual(
+ roundedReading, this.lastRoundedReading_)) {
+ // In case new rounded value is not different compared to old
+ // value, new value is not sent.
+ return;
+ }
+ this.buffer_.set(roundedReading, MockSensor.#BUFFER_OFFSET_READINGS);
+ this.lastRoundedReading_ = roundedReading;
+ }
+
+ // For all tests sensor reading should have monotonically
+ // increasing timestamp.
+ this.buffer_[MockSensor.#BUFFER_OFFSET_TIMESTAMP] = this.timestamp_++;
+
+ if (this.reportingMode_ === ReportingMode.ON_CHANGE &&
+ this.notifyOnReadingChange_) {
+ this.client_.sensorReadingChanged();
+ }
+ }, timeout);
+ }
+
+ stopReading() {
+ if (this.sensorReadingTimerId_ != null) {
+ window.clearInterval(this.sensorReadingTimerId_);
+ this.sensorReadingTimerId_ = null;
+ }
+ }
+
+ getSamplingFrequency() {
+ if (this.requestedFrequencies_.length == 0) {
+ throw new Error("getSamplingFrequency(): No configured frequency");
+ }
+ return this.requestedFrequencies_[0];
+ }
+
+ isReadingData() {
+ return this.sensorReadingTimerId_ != null;
+ }
+ }
+
+ // Class that mocks SensorProvider interface defined in
+ // https://cs.chromium.org/chromium/src/services/device/public/mojom/sensor_provider.mojom
+ class MockSensorProvider {
+ constructor() {
+ this.readingSizeInBytes_ =
+ Number(SensorInitParams_READ_BUFFER_SIZE_FOR_TESTS);
+ this.sharedBufferSizeInBytes_ =
+ this.readingSizeInBytes_ * (SensorType.MAX_VALUE + 1);
+ let rv = Mojo.createSharedBuffer(this.sharedBufferSizeInBytes_);
+ if (rv.result != Mojo.RESULT_OK) {
+ throw new Error('MockSensorProvider: Failed to create shared buffer');
+ }
+ const handle = rv.handle;
+ rv = handle.mapBuffer(0, this.sharedBufferSizeInBytes_);
+ if (rv.result != Mojo.RESULT_OK) {
+ throw new Error("MockSensorProvider: Failed to map shared buffer");
+ }
+ this.shmemArrayBuffer_ = rv.buffer;
+ rv = handle.duplicateBufferHandle({readOnly: true});
+ if (rv.result != Mojo.RESULT_OK) {
+ throw new Error(
+ 'MockSensorProvider: failed to duplicate shared buffer');
+ }
+ this.readOnlySharedBufferHandle_ = rv.handle;
+ this.activeSensors_ = new Map();
+ this.resolveFuncs_ = new Map();
+ this.getSensorShouldFail_ = new Map();
+ this.permissionsDenied_ = new Map();
+ this.maxFrequency_ = 60;
+ this.minFrequency_ = 1;
+ this.mojomSensorType_ = new Map([
+ ['Accelerometer', SensorType.ACCELEROMETER],
+ ['LinearAccelerationSensor', SensorType.LINEAR_ACCELERATION],
+ ['GravitySensor', SensorType.GRAVITY],
+ ['AmbientLightSensor', SensorType.AMBIENT_LIGHT],
+ ['Gyroscope', SensorType.GYROSCOPE],
+ ['Magnetometer', SensorType.MAGNETOMETER],
+ ['AbsoluteOrientationSensor',
+ SensorType.ABSOLUTE_ORIENTATION_QUATERNION],
+ ['AbsoluteOrientationEulerAngles',
+ SensorType.ABSOLUTE_ORIENTATION_EULER_ANGLES],
+ ['RelativeOrientationSensor',
+ SensorType.RELATIVE_ORIENTATION_QUATERNION],
+ ['RelativeOrientationEulerAngles',
+ SensorType.RELATIVE_ORIENTATION_EULER_ANGLES],
+ ['ProximitySensor', SensorType.PROXIMITY]
+ ]);
+ this.receiver_ = new SensorProviderReceiver(this);
+
+ this.interceptor_ =
+ new MojoInterfaceInterceptor(SensorProvider.$interfaceName);
+ this.interceptor_.oninterfacerequest = e => {
+ this.bindToPipe(e.handle);
+ };
+ this.interceptor_.start();
+ }
+
+ // Returns initialized Sensor proxy to the client.
+ async getSensor(type) {
+ if (this.getSensorShouldFail_.get(type)) {
+ return {result: SensorCreationResult.ERROR_NOT_AVAILABLE,
+ initParams: null};
+ }
+ if (this.permissionsDenied_.get(type)) {
+ return {result: SensorCreationResult.ERROR_NOT_ALLOWED,
+ initParams: null};
+ }
+
+ const offset = type * this.readingSizeInBytes_;
+ const reportingMode = ReportingMode.ON_CHANGE;
+
+ const sensor = new SensorRemote();
+ if (!this.activeSensors_.has(type)) {
+ const shmemView = new Float64Array(
+ this.shmemArrayBuffer_, offset,
+ this.readingSizeInBytes_ / Float64Array.BYTES_PER_ELEMENT);
+ const mockSensor = new MockSensor(
+ sensor.$.bindNewPipeAndPassReceiver(), shmemView, reportingMode,
+ type);
+ this.activeSensors_.set(type, mockSensor);
+ this.activeSensors_.get(type).client_ = new SensorClientRemote();
+ }
+
+ const rv = this.readOnlySharedBufferHandle_.duplicateBufferHandle(
+ {readOnly: true});
+ if (rv.result != Mojo.RESULT_OK) {
+ throw new Error('getSensor(): failed to duplicate shared buffer');
+ }
+
+ const defaultConfig = { frequency: DEFAULT_FREQUENCY };
+ // Consider sensor traits to meet assertions in C++ code (see
+ // services/device/public/cpp/generic_sensor/sensor_traits.h)
+ if (type == SensorType.AMBIENT_LIGHT || type == SensorType.MAGNETOMETER) {
+ this.maxFrequency_ = Math.min(10, this.maxFrequency_);
+ }
+
+ const client = this.activeSensors_.get(type).client_;
+ const initParams = {
+ sensor,
+ clientReceiver: client.$.bindNewPipeAndPassReceiver(),
+ memory: {buffer: rv.handle},
+ bufferOffset: BigInt(offset),
+ mode: reportingMode,
+ defaultConfiguration: defaultConfig,
+ minimumFrequency: this.minFrequency_,
+ maximumFrequency: this.maxFrequency_
+ };
+
+ if (this.resolveFuncs_.has(type)) {
+ for (let resolveFunc of this.resolveFuncs_.get(type)) {
+ resolveFunc(this.activeSensors_.get(type));
+ }
+ this.resolveFuncs_.delete(type);
+ }
+
+ return {result: SensorCreationResult.SUCCESS, initParams};
+ }
+
+ // Binds object to mojo message pipe
+ bindToPipe(pipe) {
+ this.receiver_.$.bindHandle(pipe);
+ }
+
+ // Mock functions
+
+ // Resets state of mock SensorProvider between test runs.
+ reset() {
+ for (const sensor of this.activeSensors_.values()) {
+ sensor.reset();
+ }
+ this.activeSensors_.clear();
+ this.resolveFuncs_.clear();
+ this.getSensorShouldFail_.clear();
+ this.permissionsDenied_.clear();
+ this.maxFrequency_ = 60;
+ this.minFrequency_ = 1;
+ this.receiver_.$.close();
+ this.interceptor_.stop();
+ }
+
+ // Sets flag that forces mock SensorProvider to fail when getSensor() is
+ // invoked.
+ setGetSensorShouldFail(sensorType, shouldFail) {
+ this.getSensorShouldFail_.set(this.mojomSensorType_.get(sensorType),
+ shouldFail);
+ }
+
+ setPermissionsDenied(sensorType, permissionsDenied) {
+ this.permissionsDenied_.set(this.mojomSensorType_.get(sensorType),
+ permissionsDenied);
+ }
+
+ // Returns mock sensor that was created in getSensor to the layout test.
+ getCreatedSensor(sensorType) {
+ const type = this.mojomSensorType_.get(sensorType);
+ if (typeof type != "number") {
+ throw new TypeError(`getCreatedSensor(): Invalid sensor type ${sensorType}`);
+ }
+
+ if (this.activeSensors_.has(type)) {
+ return Promise.resolve(this.activeSensors_.get(type));
+ }
+
+ return new Promise(resolve => {
+ if (!this.resolveFuncs_.has(type)) {
+ this.resolveFuncs_.set(type, []);
+ }
+ this.resolveFuncs_.get(type).push(resolve);
+ });
+ }
+
+ // Sets the maximum frequency for a concrete sensor.
+ setMaximumSupportedFrequency(frequency) {
+ this.maxFrequency_ = frequency;
+ }
+
+ // Sets the minimum frequency for a concrete sensor.
+ setMinimumSupportedFrequency(frequency) {
+ this.minFrequency_ = frequency;
+ }
+ }
+
+ let testInternal = {
+ initialized: false,
+ sensorProvider: null
+ }
+
+ class GenericSensorTestChromium {
+ constructor() {
+ Object.freeze(this); // Make it immutable.
+ }
+
+ async initialize() {
+ if (testInternal.initialized)
+ throw new Error('Call reset() before initialize().');
+
+ // Grant sensor permissions for Chromium testdriver.
+ // testdriver.js only works in the top-level browsing context, so do
+ // nothing if we're in e.g. an iframe.
+ if (window.parent === window) {
+ for (const entry
+ of ['accelerometer', 'gyroscope', 'magnetometer',
+ 'ambient-light-sensor']) {
+ await test_driver.set_permission({name: entry}, 'granted');
+ }
+ }
+
+ testInternal.sensorProvider = new MockSensorProvider;
+ testInternal.initialized = true;
+ }
+ // Resets state of sensor mocks between test runs.
+ async reset() {
+ if (!testInternal.initialized)
+ throw new Error('Call initialize() before reset().');
+ testInternal.sensorProvider.reset();
+ testInternal.sensorProvider = null;
+ testInternal.initialized = false;
+
+ // Wait for an event loop iteration to let any pending mojo commands in
+ // the sensor provider finish.
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+
+ getSensorProvider() {
+ return testInternal.sensorProvider;
+ }
+ }
+
+ return GenericSensorTestChromium;
+})();
diff --git a/testing/web-platform/tests/resources/chromium/generic_sensor_mocks.js.headers b/testing/web-platform/tests/resources/chromium/generic_sensor_mocks.js.headers
new file mode 100644
index 0000000000..6805c323df
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/generic_sensor_mocks.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/testing/web-platform/tests/resources/chromium/mock-barcodedetection.js b/testing/web-platform/tests/resources/chromium/mock-barcodedetection.js
new file mode 100644
index 0000000000..b0d2e0af0a
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/mock-barcodedetection.js
@@ -0,0 +1,136 @@
+import {BarcodeDetectionReceiver, BarcodeFormat} from '/gen/services/shape_detection/public/mojom/barcodedetection.mojom.m.js';
+import {BarcodeDetectionProvider, BarcodeDetectionProviderReceiver} from '/gen/services/shape_detection/public/mojom/barcodedetection_provider.mojom.m.js';
+
+self.BarcodeDetectionTest = (() => {
+ // Class that mocks BarcodeDetectionProvider interface defined in
+ // https://cs.chromium.org/chromium/src/services/shape_detection/public/mojom/barcodedetection_provider.mojom
+ class MockBarcodeDetectionProvider {
+ constructor() {
+ this.receiver_ = new BarcodeDetectionProviderReceiver(this);
+
+ this.interceptor_ = new MojoInterfaceInterceptor(
+ BarcodeDetectionProvider.$interfaceName);
+ this.interceptor_.oninterfacerequest = e => {
+ if (this.should_close_pipe_on_request_)
+ e.handle.close();
+ else
+ this.receiver_.$.bindHandle(e.handle);
+ }
+ this.interceptor_.start();
+ this.should_close_pipe_on_request_ = false;
+ }
+
+ createBarcodeDetection(request, options) {
+ this.mockService_ = new MockBarcodeDetection(request, options);
+ }
+
+ enumerateSupportedFormats() {
+ return {
+ supportedFormats: [
+ BarcodeFormat.AZTEC,
+ BarcodeFormat.DATA_MATRIX,
+ BarcodeFormat.QR_CODE,
+ ]
+ };
+ }
+
+ getFrameData() {
+ return this.mockService_.bufferData_;
+ }
+
+ getFormats() {
+ return this.mockService_.options_.formats;
+ }
+
+ reset() {
+ this.mockService_ = null;
+ this.should_close_pipe_on_request_ = false;
+ this.receiver_.$.close();
+ this.interceptor_.stop();
+ }
+
+ // simulate a 'no implementation available' case
+ simulateNoImplementation() {
+ this.should_close_pipe_on_request_ = true;
+ }
+ }
+
+ // Class that mocks BarcodeDetection interface defined in
+ // https://cs.chromium.org/chromium/src/services/shape_detection/public/mojom/barcodedetection.mojom
+ class MockBarcodeDetection {
+ constructor(request, options) {
+ this.options_ = options;
+ this.receiver_ = new BarcodeDetectionReceiver(this);
+ this.receiver_.$.bindHandle(request.handle);
+ }
+
+ detect(bitmapData) {
+ this.bufferData_ =
+ new Uint32Array(getArrayBufferFromBigBuffer(bitmapData.pixelData));
+ return {
+ results: [
+ {
+ rawValue : "cats",
+ boundingBox: { x: 1.0, y: 1.0, width: 100.0, height: 100.0 },
+ format: BarcodeFormat.QR_CODE,
+ cornerPoints: [
+ { x: 1.0, y: 1.0 },
+ { x: 101.0, y: 1.0 },
+ { x: 101.0, y: 101.0 },
+ { x: 1.0, y: 101.0 }
+ ],
+ },
+ {
+ rawValue : "dogs",
+ boundingBox: { x: 2.0, y: 2.0, width: 50.0, height: 50.0 },
+ format: BarcodeFormat.CODE_128,
+ cornerPoints: [
+ { x: 2.0, y: 2.0 },
+ { x: 52.0, y: 2.0 },
+ { x: 52.0, y: 52.0 },
+ { x: 2.0, y: 52.0 }
+ ],
+ },
+ ],
+ };
+ }
+ }
+
+ let testInternal = {
+ initialized: false,
+ MockBarcodeDetectionProvider: null
+ }
+
+ class BarcodeDetectionTestChromium {
+ constructor() {
+ Object.freeze(this); // Make it immutable.
+ }
+
+ initialize() {
+ if (testInternal.initialized)
+ throw new Error('Call reset() before initialize().');
+
+ testInternal.MockBarcodeDetectionProvider = new MockBarcodeDetectionProvider;
+ testInternal.initialized = true;
+ }
+
+ // Resets state of barcode detection mocks between test runs.
+ async reset() {
+ if (!testInternal.initialized)
+ throw new Error('Call initialize() before reset().');
+ testInternal.MockBarcodeDetectionProvider.reset();
+ testInternal.MockBarcodeDetectionProvider = null;
+ testInternal.initialized = false;
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+
+ MockBarcodeDetectionProvider() {
+ return testInternal.MockBarcodeDetectionProvider;
+ }
+ }
+
+ return BarcodeDetectionTestChromium;
+})();
+
+self.BarcodeFormat = BarcodeFormat;
diff --git a/testing/web-platform/tests/resources/chromium/mock-barcodedetection.js.headers b/testing/web-platform/tests/resources/chromium/mock-barcodedetection.js.headers
new file mode 100644
index 0000000000..6c61a34a4e
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/mock-barcodedetection.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8 \ No newline at end of file
diff --git a/testing/web-platform/tests/resources/chromium/mock-battery-monitor.headers b/testing/web-platform/tests/resources/chromium/mock-battery-monitor.headers
new file mode 100644
index 0000000000..6805c323df
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/mock-battery-monitor.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/testing/web-platform/tests/resources/chromium/mock-battery-monitor.js b/testing/web-platform/tests/resources/chromium/mock-battery-monitor.js
new file mode 100644
index 0000000000..8fa27bc56a
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/mock-battery-monitor.js
@@ -0,0 +1,61 @@
+import {BatteryMonitor, BatteryMonitorReceiver} from '/gen/services/device/public/mojom/battery_monitor.mojom.m.js';
+
+class MockBatteryMonitor {
+ constructor() {
+ this.receiver_ = new BatteryMonitorReceiver(this);
+ this.interceptor_ =
+ new MojoInterfaceInterceptor(BatteryMonitor.$interfaceName);
+ this.interceptor_.oninterfacerequest = e =>
+ this.receiver_.$.bindHandle(e.handle);
+ this.reset();
+ }
+
+ start() {
+ this.interceptor_.start();
+ }
+
+ stop() {
+ this.interceptor_.stop();
+ }
+
+ reset() {
+ this.pendingRequests_ = [];
+ this.status_ = null;
+ this.lastKnownStatus_ = null;
+ }
+
+ queryNextStatus() {
+ const result = new Promise(resolve => this.pendingRequests_.push(resolve));
+ this.runCallbacks_();
+ return result;
+ }
+
+ setBatteryStatus(charging, chargingTime, dischargingTime, level) {
+ this.status_ = {charging, chargingTime, dischargingTime, level};
+ this.lastKnownStatus_ = this.status_;
+ this.runCallbacks_();
+ }
+
+ verifyBatteryStatus(manager) {
+ assert_not_equals(manager, undefined);
+ assert_not_equals(this.lastKnownStatus_, null);
+ assert_equals(manager.charging, this.lastKnownStatus_.charging);
+ assert_equals(manager.chargingTime, this.lastKnownStatus_.chargingTime);
+ assert_equals(
+ manager.dischargingTime, this.lastKnownStatus_.dischargingTime);
+ assert_equals(manager.level, this.lastKnownStatus_.level);
+ }
+
+ runCallbacks_() {
+ if (!this.status_ || !this.pendingRequests_.length)
+ return;
+
+ let result = {status: this.status_};
+ while (this.pendingRequests_.length) {
+ this.pendingRequests_.pop()(result);
+ }
+ this.status_ = null;
+ }
+}
+
+export const mockBatteryMonitor = new MockBatteryMonitor();
diff --git a/testing/web-platform/tests/resources/chromium/mock-direct-sockets.js b/testing/web-platform/tests/resources/chromium/mock-direct-sockets.js
new file mode 100644
index 0000000000..6d557f7a01
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/mock-direct-sockets.js
@@ -0,0 +1,94 @@
+'use strict';
+
+import {DirectSocketsService, DirectSocketsServiceReceiver} from '/gen/third_party/blink/public/mojom/direct_sockets/direct_sockets.mojom.m.js';
+
+self.DirectSocketsServiceTest = (() => {
+ // Class that mocks DirectSocketsService interface defined in
+ // https://source.chromium.org/chromium/chromium/src/third_party/blink/public/mojom/direct_sockets/direct_sockets.mojom
+ class MockDirectSocketsService {
+ constructor() {
+ this.interceptor_ = new MojoInterfaceInterceptor(DirectSocketsService.$interfaceName);
+ this.receiver_ = new DirectSocketsServiceReceiver(this);
+ this.interceptor_.oninterfacerequest = e =>
+ this.receiver_.$.bindHandle(e.handle);
+ this.interceptor_.start();
+ }
+
+ reset() {
+ this.receiver_.$.close();
+ this.interceptor_.stop();
+ }
+
+ openTCPSocket(
+ options,
+ receiver,
+ observer) {
+ return Promise.resolve({
+ // return result = net:Error::NOT_IMPLEMENTED (code -11)
+ result: -11
+ });
+ }
+
+ openConnectedUDPSocket(
+ options,
+ receiver,
+ listener) {
+ return Promise.resolve({
+ // return result = net:Error::NOT_IMPLEMENTED (code -11)
+ result: -11
+ });
+ }
+
+ openBoundUDPSocket(
+ options,
+ receiver,
+ listener) {
+ return Promise.resolve({
+ // return result = net:Error::NOT_IMPLEMENTED (code -11)
+ result: -11
+ });
+ }
+
+ openTCPServerSocket(
+ options,
+ receiver) {
+ return Promise.resolve({
+ // return result = net:Error::NOT_IMPLEMENTED (code -11)
+ result: -11
+ });
+ }
+ }
+
+ let testInternal = {
+ initialized: false,
+ mockDirectSocketsService: null
+ }
+
+ class DirectSocketsServiceTestChromium {
+ constructor() {
+ Object.freeze(this); // Make it immutable.
+ }
+
+ initialize() {
+ if (!testInternal.initialized) {
+ testInternal = {
+ mockDirectSocketsService: new MockDirectSocketsService(),
+ initialized: true
+ };
+ }
+ }
+
+ async reset() {
+ if (testInternal.initialized) {
+ testInternal.mockDirectSocketsService.reset();
+ testInternal = {
+ mockDirectSocketsService: null,
+ initialized: false
+ };
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+ }
+ }
+
+ return DirectSocketsServiceTestChromium;
+})();
diff --git a/testing/web-platform/tests/resources/chromium/mock-facedetection.js b/testing/web-platform/tests/resources/chromium/mock-facedetection.js
new file mode 100644
index 0000000000..7ae658621e
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/mock-facedetection.js
@@ -0,0 +1,130 @@
+import {FaceDetectionReceiver, LandmarkType} from '/gen/services/shape_detection/public/mojom/facedetection.mojom.m.js';
+import {FaceDetectionProvider, FaceDetectionProviderReceiver} from '/gen/services/shape_detection/public/mojom/facedetection_provider.mojom.m.js';
+
+self.FaceDetectionTest = (() => {
+ // Class that mocks FaceDetectionProvider interface defined in
+ // https://cs.chromium.org/chromium/src/services/shape_detection/public/mojom/facedetection_provider.mojom
+ class MockFaceDetectionProvider {
+ constructor() {
+ this.receiver_ = new FaceDetectionProviderReceiver(this);
+
+ this.interceptor_ = new MojoInterfaceInterceptor(
+ FaceDetectionProvider.$interfaceName);
+ this.interceptor_.oninterfacerequest =
+ e => this.receiver_.$.bindHandle(e.handle);
+ this.interceptor_.start();
+ }
+
+ createFaceDetection(request, options) {
+ this.mockService_ = new MockFaceDetection(request, options);
+ }
+
+ getFrameData() {
+ return this.mockService_.bufferData_;
+ }
+
+ getMaxDetectedFaces() {
+ return this.mockService_.maxDetectedFaces_;
+ }
+
+ getFastMode () {
+ return this.mockService_.fastMode_;
+ }
+
+ reset() {
+ this.mockService_ = null;
+ this.receiver_.$.close();
+ this.interceptor_.stop();
+ }
+ }
+
+ // Class that mocks FaceDetection interface defined in
+ // https://cs.chromium.org/chromium/src/services/shape_detection/public/mojom/facedetection.mojom
+ class MockFaceDetection {
+ constructor(request, options) {
+ this.maxDetectedFaces_ = options.maxDetectedFaces;
+ this.fastMode_ = options.fastMode;
+ this.receiver_ = new FaceDetectionReceiver(this);
+ this.receiver_.$.bindHandle(request.handle);
+ }
+
+ detect(bitmapData) {
+ this.bufferData_ =
+ new Uint32Array(getArrayBufferFromBigBuffer(bitmapData.pixelData));
+ return Promise.resolve({
+ results: [
+ {
+ boundingBox: {x: 1.0, y: 1.0, width: 100.0, height: 100.0},
+ landmarks: [{
+ type: LandmarkType.EYE,
+ locations: [{x: 4.0, y: 5.0}]
+ },
+ {
+ type: LandmarkType.EYE,
+ locations: [
+ {x: 4.0, y: 5.0}, {x: 5.0, y: 4.0}, {x: 6.0, y: 3.0},
+ {x: 7.0, y: 4.0}, {x: 8.0, y: 5.0}, {x: 7.0, y: 6.0},
+ {x: 6.0, y: 7.0}, {x: 5.0, y: 6.0}
+ ]
+ }]
+ },
+ {
+ boundingBox: {x: 2.0, y: 2.0, width: 200.0, height: 200.0},
+ landmarks: [{
+ type: LandmarkType.NOSE,
+ locations: [{x: 100.0, y: 50.0}]
+ },
+ {
+ type: LandmarkType.NOSE,
+ locations: [
+ {x: 80.0, y: 50.0}, {x: 70.0, y: 60.0}, {x: 60.0, y: 70.0},
+ {x: 70.0, y: 60.0}, {x: 80.0, y: 70.0}, {x: 90.0, y: 80.0},
+ {x: 100.0, y: 70.0}, {x: 90.0, y: 60.0}, {x: 80.0, y: 50.0}
+ ]
+ }]
+ },
+ {
+ boundingBox: {x: 3.0, y: 3.0, width: 300.0, height: 300.0},
+ landmarks: []
+ },
+ ]
+ });
+ }
+ }
+
+ let testInternal = {
+ initialized: false,
+ MockFaceDetectionProvider: null
+ }
+
+ class FaceDetectionTestChromium {
+ constructor() {
+ Object.freeze(this); // Make it immutable.
+ }
+
+ initialize() {
+ if (testInternal.initialized)
+ throw new Error('Call reset() before initialize().');
+
+ testInternal.MockFaceDetectionProvider = new MockFaceDetectionProvider;
+ testInternal.initialized = true;
+ }
+
+ // Resets state of face detection mocks between test runs.
+ async reset() {
+ if (!testInternal.initialized)
+ throw new Error('Call initialize() before reset().');
+ testInternal.MockFaceDetectionProvider.reset();
+ testInternal.MockFaceDetectionProvider = null;
+ testInternal.initialized = false;
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+
+ MockFaceDetectionProvider() {
+ return testInternal.MockFaceDetectionProvider;
+ }
+ }
+
+ return FaceDetectionTestChromium;
+})();
diff --git a/testing/web-platform/tests/resources/chromium/mock-facedetection.js.headers b/testing/web-platform/tests/resources/chromium/mock-facedetection.js.headers
new file mode 100644
index 0000000000..6c61a34a4e
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/mock-facedetection.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8 \ No newline at end of file
diff --git a/testing/web-platform/tests/resources/chromium/mock-idle-detection.js b/testing/web-platform/tests/resources/chromium/mock-idle-detection.js
new file mode 100644
index 0000000000..54fe5dd01e
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/mock-idle-detection.js
@@ -0,0 +1,80 @@
+import {IdleManager, IdleManagerError, IdleManagerReceiver} from '/gen/third_party/blink/public/mojom/idle/idle_manager.mojom.m.js';
+
+/**
+ * This is a testing framework that enables us to test the user idle detection
+ * by intercepting the connection between the renderer and the browser and
+ * exposing a mocking API for tests.
+ *
+ * Usage:
+ *
+ * 1) Include <script src="mock.js"></script> in your file.
+ * 2) Set expectations
+ * expect(addMonitor).andReturn((threshold, monitorPtr, callback) => {
+ * // mock behavior
+ * })
+ * 3) Call navigator.idle.query()
+ *
+ * The mocking API is blink agnostic and is designed such that other engines
+ * could implement it too. Here are the symbols that are exposed to tests:
+ *
+ * - function addMonitor(): the main/only function that can be mocked.
+ * - function expect(): the main/only function that enables us to mock it.
+ * - function close(): disconnects the interceptor.
+ * - enum UserIdleState {IDLE, ACTIVE}: blink agnostic constants.
+ * - enum ScreenIdleState {LOCKED, UNLOCKED}: blink agnostic constants.
+ */
+
+class FakeIdleMonitor {
+ addMonitor(threshold, monitorPtr, callback) {
+ return this.handler.addMonitor(threshold, monitorPtr);
+ }
+ setHandler(handler) {
+ this.handler = handler;
+ return this;
+ }
+ setBinding(binding) {
+ this.binding = binding;
+ return this;
+ }
+ close() {
+ this.binding.$.close();
+ }
+}
+
+self.IdleDetectorError = {};
+
+self.addMonitor = function addMonitor(threshold, monitorPtr, callback) {
+ throw new Error("expected to be overriden by tests");
+}
+
+async function close() {
+ interceptor.close();
+}
+
+self.expect = function(call) {
+ return {
+ andReturn(callback) {
+ let handler = {};
+ handler[call.name] = callback;
+ interceptor.setHandler(handler);
+ }
+ };
+};
+
+function intercept() {
+ let result = new FakeIdleMonitor();
+
+ let binding = new IdleManagerReceiver(result);
+ let interceptor = new MojoInterfaceInterceptor(IdleManager.$interfaceName);
+ interceptor.oninterfacerequest = e => binding.$.bindHandle(e.handle);
+ interceptor.start();
+
+ self.IdleDetectorError.SUCCESS = IdleManagerError.kSuccess;
+ self.IdleDetectorError.PERMISSION_DISABLED =
+ IdleManagerError.kPermissionDisabled;
+
+ result.setBinding(binding);
+ return result;
+}
+
+const interceptor = intercept();
diff --git a/testing/web-platform/tests/resources/chromium/mock-imagecapture.js b/testing/web-platform/tests/resources/chromium/mock-imagecapture.js
new file mode 100644
index 0000000000..8424e1e36c
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/mock-imagecapture.js
@@ -0,0 +1,309 @@
+import {BackgroundBlurMode, FillLightMode, ImageCapture, ImageCaptureReceiver, MeteringMode, RedEyeReduction} from '/gen/media/capture/mojom/image_capture.mojom.m.js';
+
+self.ImageCaptureTest = (() => {
+ // Class that mocks ImageCapture interface defined in
+ // https://cs.chromium.org/chromium/src/media/capture/mojom/image_capture.mojom
+ class MockImageCapture {
+ constructor() {
+ this.interceptor_ =
+ new MojoInterfaceInterceptor(ImageCapture.$interfaceName);
+ this.interceptor_.oninterfacerequest =
+ e => this.receiver_.$.bindHandle(e.handle);
+ this.interceptor_.start();
+
+ this.state_ = {
+ state: {
+ supportedWhiteBalanceModes: [
+ MeteringMode.SINGLE_SHOT,
+ MeteringMode.CONTINUOUS
+ ],
+ currentWhiteBalanceMode: MeteringMode.CONTINUOUS,
+ supportedExposureModes: [
+ MeteringMode.MANUAL,
+ MeteringMode.SINGLE_SHOT,
+ MeteringMode.CONTINUOUS
+ ],
+ currentExposureMode: MeteringMode.MANUAL,
+ supportedFocusModes: [
+ MeteringMode.MANUAL,
+ MeteringMode.SINGLE_SHOT
+ ],
+ currentFocusMode: MeteringMode.MANUAL,
+ pointsOfInterest: [{
+ x: 0.4,
+ y: 0.6
+ }],
+
+ exposureCompensation: {
+ min: -200.0,
+ max: 200.0,
+ current: 33.0,
+ step: 33.0
+ },
+ exposureTime: {
+ min: 100.0,
+ max: 100000.0,
+ current: 1000.0,
+ step: 100.0
+ },
+ colorTemperature: {
+ min: 2500.0,
+ max: 6500.0,
+ current: 6000.0,
+ step: 1000.0
+ },
+ iso: {
+ min: 100.0,
+ max: 12000.0,
+ current: 400.0,
+ step: 1.0
+ },
+
+ brightness: {
+ min: 1.0,
+ max: 10.0,
+ current: 5.0,
+ step: 1.0
+ },
+ contrast: {
+ min: 2.0,
+ max: 9.0,
+ current: 5.0,
+ step: 1.0
+ },
+ saturation: {
+ min: 3.0,
+ max: 8.0,
+ current: 6.0,
+ step: 1.0
+ },
+ sharpness: {
+ min: 4.0,
+ max: 7.0,
+ current: 7.0,
+ step: 1.0
+ },
+
+ focusDistance: {
+ min: 1.0,
+ max: 10.0,
+ current: 3.0,
+ step: 1.0
+ },
+
+ pan: {
+ min: 0.0,
+ max: 10.0,
+ current: 5.0,
+ step: 2.0
+ },
+
+ tilt: {
+ min: 0.0,
+ max: 10.0,
+ current: 5.0,
+ step: 2.0
+ },
+
+ zoom: {
+ min: 0.0,
+ max: 10.0,
+ current: 5.0,
+ step: 5.0
+ },
+
+ supportsTorch: true,
+ torch: false,
+
+ redEyeReduction: RedEyeReduction.CONTROLLABLE,
+ height: {
+ min: 240.0,
+ max: 2448.0,
+ current: 240.0,
+ step: 2.0
+ },
+ width: {
+ min: 320.0,
+ max: 3264.0,
+ current: 320.0,
+ step: 3.0
+ },
+ fillLightMode: [FillLightMode.AUTO, FillLightMode.FLASH],
+
+ supportedBackgroundBlurModes: [
+ BackgroundBlurMode.OFF,
+ BackgroundBlurMode.BLUR
+ ],
+ backgroundBlurMode: BackgroundBlurMode.OFF,
+ }
+ };
+ this.panTiltZoomPermissionStatus_ = null;
+ this.settings_ = null;
+ this.receiver_ = new ImageCaptureReceiver(this);
+ }
+
+ reset() {
+ this.receiver_.$.close();
+ this.interceptor_.stop();
+ }
+
+ async getPhotoState(source_id) {
+ const shouldKeepPanTiltZoom = await this.isPanTiltZoomPermissionGranted();
+ if (shouldKeepPanTiltZoom)
+ return Promise.resolve(this.state_);
+
+ const newState = {...this.state_};
+ newState.state.pan = {};
+ newState.state.tilt = {};
+ newState.state.zoom = {};
+ return Promise.resolve(newState);
+ }
+
+ async setPhotoOptions(source_id, settings) {
+ const isAllowedToControlPanTiltZoom = await this.isPanTiltZoomPermissionGranted();
+ if (!isAllowedToControlPanTiltZoom &&
+ (settings.hasPan || settings.hasTilt || settings.hasZoom)) {
+ return Promise.resolve({ success: false });
+ }
+ this.settings_ = settings;
+ if (settings.hasIso)
+ this.state_.state.iso.current = settings.iso;
+ if (settings.hasHeight)
+ this.state_.state.height.current = settings.height;
+ if (settings.hasWidth)
+ this.state_.state.width.current = settings.width;
+ if (settings.hasPan)
+ this.state_.state.pan.current = settings.pan;
+ if (settings.hasTilt)
+ this.state_.state.tilt.current = settings.tilt;
+ if (settings.hasZoom)
+ this.state_.state.zoom.current = settings.zoom;
+ if (settings.hasFocusMode)
+ this.state_.state.currentFocusMode = settings.focusMode;
+ if (settings.hasFocusDistance)
+ this.state_.state.focusDistance.current = settings.focusDistance;
+
+ if (settings.pointsOfInterest.length > 0) {
+ this.state_.state.pointsOfInterest =
+ settings.pointsOfInterest;
+ }
+
+ if (settings.hasExposureMode)
+ this.state_.state.currentExposureMode = settings.exposureMode;
+
+ if (settings.hasExposureCompensation) {
+ this.state_.state.exposureCompensation.current =
+ settings.exposureCompensation;
+ }
+ if (settings.hasExposureTime) {
+ this.state_.state.exposureTime.current =
+ settings.exposureTime;
+ }
+ if (settings.hasWhiteBalanceMode) {
+ this.state_.state.currentWhiteBalanceMode =
+ settings.whiteBalanceMode;
+ }
+ if (settings.hasFillLightMode)
+ this.state_.state.fillLightMode = [settings.fillLightMode];
+ if (settings.hasRedEyeReduction)
+ this.state_.state.redEyeReduction = settings.redEyeReduction;
+ if (settings.hasColorTemperature) {
+ this.state_.state.colorTemperature.current =
+ settings.colorTemperature;
+ }
+ if (settings.hasBrightness)
+ this.state_.state.brightness.current = settings.brightness;
+ if (settings.hasContrast)
+ this.state_.state.contrast.current = settings.contrast;
+ if (settings.hasSaturation)
+ this.state_.state.saturation.current = settings.saturation;
+ if (settings.hasSharpness)
+ this.state_.state.sharpness.current = settings.sharpness;
+
+ if (settings.hasTorch)
+ this.state_.state.torch = settings.torch;
+
+ if (settings.hasBackgroundBlurMode)
+ this.state_.state.backgroundBlurMode = [settings.backgroundBlurMode];
+
+ return Promise.resolve({
+ success: true
+ });
+ }
+
+ takePhoto(source_id) {
+ return Promise.resolve({
+ blob: {
+ mimeType: 'image/cat',
+ data: new Array(2)
+ }
+ });
+ }
+
+ async isPanTiltZoomPermissionGranted() {
+ if (!this.panTiltZoomPermissionStatus_) {
+ this.panTiltZoomPermissionStatus_ = await navigator.permissions.query({
+ name: "camera",
+ panTiltZoom: true
+ });
+ }
+ return this.panTiltZoomPermissionStatus_.state == "granted";
+ }
+
+ state() {
+ return this.state_.state;
+ }
+
+ turnOffBackgroundBlurMode() {
+ this.state_.state.backgroundBlurMode = BackgroundBlurMode.OFF;
+ }
+ turnOnBackgroundBlurMode() {
+ this.state_.state.backgroundBlurMode = BackgroundBlurMode.BLUR;
+ }
+ turnOffSupportedBackgroundBlurModes() {
+ this.state_.state.supportedBackgroundBlurModes = [BackgroundBlurMode.OFF];
+ }
+ turnOnSupportedBackgroundBlurModes() {
+ this.state_.state.supportedBackgroundBlurModes = [BackgroundBlurMode.BLUR];
+ }
+
+ options() {
+ return this.settings_;
+ }
+ }
+
+ let testInternal = {
+ initialized: false,
+ mockImageCapture: null
+ }
+
+ class ImageCaptureTestChromium {
+
+ constructor() {
+ Object.freeze(this); // Make it immutable.
+ }
+
+ initialize() {
+ if (testInternal.initialized)
+ throw new Error('Call reset() before initialize().');
+
+ testInternal.mockImageCapture = new MockImageCapture;
+ testInternal.initialized = true;
+ }
+ // Resets state of image capture mocks between test runs.
+ async reset() {
+ if (!testInternal.initialized)
+ throw new Error('Call initialize() before reset().');
+ testInternal.mockImageCapture.reset();
+ testInternal.mockImageCapture = null;
+ testInternal.initialized = false;
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+ mockImageCapture() {
+ return testInternal.mockImageCapture;
+ }
+ }
+
+ return ImageCaptureTestChromium;
+})();
diff --git a/testing/web-platform/tests/resources/chromium/mock-managed-config.js b/testing/web-platform/tests/resources/chromium/mock-managed-config.js
new file mode 100644
index 0000000000..c9980e1285
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/mock-managed-config.js
@@ -0,0 +1,91 @@
+'use strict'
+
+import{ManagedConfigurationObserverRemote, ManagedConfigurationService, ManagedConfigurationServiceReceiver} from '/gen/third_party/blink/public/mojom/device/device.mojom.m.js';
+
+
+self.ManagedConfigTest = (() => {
+ // Class that mocks ManagedConfigurationService interface defined in
+ // https://source.chromium.org/chromium/chromium/src/third_party/blink/public/mojom/device/device.mojom
+ class MockManagedConfig {
+ constructor() {
+ this.receiver_ = new ManagedConfigurationServiceReceiver(this);
+ this.interceptor_ = new MojoInterfaceInterceptor(
+ ManagedConfigurationService.$interfaceName);
+ this.interceptor_.oninterfacerequest = e =>
+ this.receiver_.$.bindHandle(e.handle);
+ this.interceptor_.start();
+ this.subscription_ = null;
+ this.reset();
+ }
+
+ reset() {
+ this.configuration_ = null;
+ this.onObserverAdd_ = null;
+ }
+
+ async getManagedConfiguration(keys) {
+ if (this.configuration_ === null) {
+ return {};
+ }
+
+ return {
+ configurations: Object.keys(this.configuration_)
+ .filter(key => keys.includes(key))
+ .reduce(
+ (obj, key) => {
+ obj[key] =
+ JSON.stringify(this.configuration_[key]);
+ return obj;
+ },
+ {})
+ };
+ }
+
+ subscribeToManagedConfiguration(remote) {
+ this.subscription_ = remote;
+ if (this.onObserverAdd_ !== null) {
+ this.onObserverAdd_();
+ }
+ }
+
+ setManagedConfig(value) {
+ this.configuration_ = value;
+ if (this.subscription_ !== null) {
+ this.subscription_.onConfigurationChanged();
+ }
+ }
+ }
+
+ let testInternal = {
+ initialized: false,
+ mockManagedConfig: null
+ }
+
+ class ManagedConfigTestChromium {
+ constructor() {
+ Object.freeze(this); // Make it immutable.
+ }
+
+ initialize() {
+ if (testInternal.mockManagedConfig !== null) {
+ testInternal.mockManagedConfig.reset();
+ return;
+ }
+
+ testInternal.mockManagedConfig = new MockManagedConfig;
+ testInternal.initialized = true;
+ }
+
+ setManagedConfig(config) {
+ testInternal.mockManagedConfig.setManagedConfig(config);
+ }
+
+ async nextObserverAdded() {
+ await new Promise(resolve => {
+ testInternal.mockManagedConfig.onObserverAdd_ = resolve;
+ });
+ }
+ }
+
+ return ManagedConfigTestChromium;
+})();
diff --git a/testing/web-platform/tests/resources/chromium/mock-pressure-service.js b/testing/web-platform/tests/resources/chromium/mock-pressure-service.js
new file mode 100644
index 0000000000..02d10f856a
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/mock-pressure-service.js
@@ -0,0 +1,134 @@
+import {PressureManager, PressureManagerReceiver, PressureStatus} from '/gen/services/device/public/mojom/pressure_manager.mojom.m.js'
+import {PressureSource, PressureState} from '/gen/services/device/public/mojom/pressure_update.mojom.m.js'
+
+class MockPressureService {
+ constructor() {
+ this.receiver_ = new PressureManagerReceiver(this);
+ this.interceptor_ =
+ new MojoInterfaceInterceptor(PressureManager.$interfaceName);
+ this.interceptor_.oninterfacerequest = e => {
+ this.receiver_.$.bindHandle(e.handle);
+ };
+ this.reset();
+ this.mojomSourceType_ = new Map([['cpu', PressureSource.kCpu]]);
+ this.mojomStateType_ = new Map([
+ ['nominal', PressureState.kNominal], ['fair', PressureState.kFair],
+ ['serious', PressureState.kSerious], ['critical', PressureState.kCritical]
+ ]);
+ this.pressureServiceReadingTimerId_ = null;
+ }
+
+ start() {
+ this.interceptor_.start();
+ }
+
+ stop() {
+ this.stopPlatformCollector();
+ this.receiver_.$.close();
+ this.interceptor_.stop();
+
+ // Wait for an event loop iteration to let any pending mojo commands in
+ // the pressure service finish.
+ return new Promise(resolve => setTimeout(resolve, 0));
+ }
+
+ reset() {
+ this.observers_ = [];
+ this.pressureUpdate_ = null;
+ this.pressureServiceReadingTimerId_ = null;
+ this.pressureStatus_ = PressureStatus.kOk;
+ this.updatesDelivered_ = 0;
+ }
+
+ async addClient(observer, source) {
+ if (this.observers_.indexOf(observer) >= 0)
+ throw new Error('addClient() has already been called');
+
+ // TODO(crbug.com/1342184): Consider other sources.
+ // For now, "cpu" is the only source.
+ if (source !== PressureSource.kCpu)
+ throw new Error('Call addClient() with a wrong PressureSource');
+
+ observer.onConnectionError.addListener(() => {
+ // Remove this observer from observer array.
+ this.observers_.splice(this.observers_.indexOf(observer), 1);
+ });
+ this.observers_.push(observer);
+
+ return {status: this.pressureStatus_};
+ }
+
+ startPlatformCollector(sampleRate) {
+ if (sampleRate === 0)
+ return;
+
+ if (this.pressureServiceReadingTimerId_ != null)
+ this.stopPlatformCollector();
+
+ // The following code for calculating the timestamp was taken from
+ // https://source.chromium.org/chromium/chromium/src/+/main:third_party/
+ // blink/web_tests/http/tests/resources/
+ // geolocation-mock.js;l=131;drc=37a9b6c03b9bda9fcd62fc0e5e8016c278abd31f
+
+ // The new Date().getTime() returns the number of milliseconds since the
+ // UNIX epoch (1970-01-01 00::00:00 UTC), while |internalValue| of the
+ // device.mojom.PressureUpdate represents the value of microseconds since
+ // the Windows FILETIME epoch (1601-01-01 00:00:00 UTC). So add the delta
+ // when sets the |internalValue|. See more info in //base/time/time.h.
+ const windowsEpoch = Date.UTC(1601, 0, 1, 0, 0, 0, 0);
+ const unixEpoch = Date.UTC(1970, 0, 1, 0, 0, 0, 0);
+ // |epochDeltaInMs| equals to base::Time::kTimeTToMicrosecondsOffset.
+ const epochDeltaInMs = unixEpoch - windowsEpoch;
+
+ const timeout = (1 / sampleRate) * 1000;
+ this.pressureServiceReadingTimerId_ = self.setInterval(() => {
+ if (this.pressureUpdate_ === null || this.observers_.length === 0)
+ return;
+ this.pressureUpdate_.timestamp = {
+ internalValue: BigInt((new Date().getTime() + epochDeltaInMs) * 1000)
+ };
+ for (let observer of this.observers_)
+ observer.onPressureUpdated(this.pressureUpdate_);
+ this.updatesDelivered_++;
+ }, timeout);
+ }
+
+ stopPlatformCollector() {
+ if (this.pressureServiceReadingTimerId_ != null) {
+ self.clearInterval(this.pressureServiceReadingTimerId_);
+ this.pressureServiceReadingTimerId_ = null;
+ }
+ this.updatesDelivered_ = 0;
+ }
+
+ updatesDelivered() {
+ return this.updatesDelivered_;
+ }
+
+ setPressureUpdate(source, state) {
+ if (!this.mojomSourceType_.has(source))
+ throw new Error(`PressureSource '${source}' is invalid`);
+
+ if (!this.mojomStateType_.has(state))
+ throw new Error(`PressureState '${state}' is invalid`);
+
+ this.pressureUpdate_ = {
+ source: this.mojomSourceType_.get(source),
+ state: this.mojomStateType_.get(state),
+ };
+ }
+
+ setExpectedFailure(expectedException) {
+ assert_true(
+ expectedException instanceof DOMException,
+ 'setExpectedFailure() expects a DOMException instance');
+ if (expectedException.name === 'NotSupportedError') {
+ this.pressureStatus_ = PressureStatus.kNotSupported;
+ } else {
+ throw new TypeError(
+ `Unexpected DOMException '${expectedException.name}'`);
+ }
+ }
+}
+
+export const mockPressureService = new MockPressureService();
diff --git a/testing/web-platform/tests/resources/chromium/mock-pressure-service.js.headers b/testing/web-platform/tests/resources/chromium/mock-pressure-service.js.headers
new file mode 100644
index 0000000000..6805c323df
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/mock-pressure-service.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/testing/web-platform/tests/resources/chromium/mock-subapps.js b/testing/web-platform/tests/resources/chromium/mock-subapps.js
new file mode 100644
index 0000000000..b81936713b
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/mock-subapps.js
@@ -0,0 +1,89 @@
+'use strict';
+
+import {SubAppsService, SubAppsServiceReceiver, SubAppsServiceResultCode} from '/gen/third_party/blink/public/mojom/subapps/sub_apps_service.mojom.m.js';
+
+self.SubAppsServiceTest = (() => {
+ // Class that mocks SubAppsService interface defined in /third_party/blink/public/mojom/subapps/sub_apps_service.mojom
+
+ class MockSubAppsService {
+ constructor() {
+ this.interceptor_ =
+ new MojoInterfaceInterceptor(SubAppsService.$interfaceName);
+ this.receiver_ = new SubAppsServiceReceiver(this);
+ this.interceptor_.oninterfacerequest =
+ e => this.receiver_.$.bindHandle(e.handle);
+ this.interceptor_.start();
+ }
+
+ reset() {
+ this.interceptor_.stop();
+ this.receiver_.$.close();
+ }
+
+ add(sub_apps) {
+ return Promise.resolve({
+ result: testInternal.addCallReturnValue,
+ });
+ }
+
+ list() {
+ return Promise.resolve({
+ result: {
+ resultCode: testInternal.serviceResultCode,
+ subAppsList: testInternal.listCallReturnValue,
+ }
+ });
+ }
+
+ remove() {
+ return Promise.resolve({
+ result: testInternal.removeCallReturnValue,
+ });
+ }
+ }
+
+ let testInternal = {
+ initialized: false,
+ mockSubAppsService: null,
+ serviceResultCode: 0,
+ addCallReturnValue: [],
+ listCallReturnValue: [],
+ removeCallReturnValue: [],
+ }
+
+ class SubAppsServiceTestChromium {
+ constructor() {
+ Object.freeze(this); // Make it immutable.
+ }
+
+ initialize(service_result_code, add_call_return_value, list_call_return_value, remove_call_return_value) {
+ if (!testInternal.initialized) {
+ testInternal = {
+ mockSubAppsService: new MockSubAppsService(),
+ initialized: true,
+ serviceResultCode: service_result_code,
+ addCallReturnValue: add_call_return_value,
+ listCallReturnValue: list_call_return_value,
+ removeCallReturnValue: remove_call_return_value,
+ };
+ };
+ }
+
+ async reset() {
+ if (testInternal.initialized) {
+ testInternal.mockSubAppsService.reset();
+ testInternal = {
+ mockSubAppsService: null,
+ initialized: false,
+ serviceResultCode: 0,
+ addCallReturnValue: [],
+ listCallReturnValue: [],
+ removeCallReturnValue: [],
+ };
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+ }
+ }
+
+ return SubAppsServiceTestChromium;
+})();
diff --git a/testing/web-platform/tests/resources/chromium/mock-textdetection.js b/testing/web-platform/tests/resources/chromium/mock-textdetection.js
new file mode 100644
index 0000000000..52ca987e28
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/mock-textdetection.js
@@ -0,0 +1,92 @@
+import {TextDetection, TextDetectionReceiver} from '/gen/services/shape_detection/public/mojom/textdetection.mojom.m.js';
+
+self.TextDetectionTest = (() => {
+ // Class that mocks TextDetection interface defined in
+ // https://cs.chromium.org/chromium/src/services/shape_detection/public/mojom/textdetection.mojom
+ class MockTextDetection {
+ constructor() {
+ this.receiver_ = new TextDetectionReceiver(this);
+ this.interceptor_ =
+ new MojoInterfaceInterceptor(TextDetection.$interfaceName);
+ this.interceptor_.oninterfacerequest =
+ e => this.receiver_.$.bindHandle(e.handle);
+ this.interceptor_.start();
+ }
+
+ detect(bitmapData) {
+ this.bufferData_ =
+ new Uint32Array(getArrayBufferFromBigBuffer(bitmapData.pixelData));
+ return Promise.resolve({
+ results: [
+ {
+ rawValue : "cats",
+ boundingBox: { x: 1.0, y: 1.0, width: 100.0, height: 100.0 },
+ cornerPoints: [
+ { x: 1.0, y: 1.0 },
+ { x: 101.0, y: 1.0 },
+ { x: 101.0, y: 101.0 },
+ { x: 1.0, y: 101.0 }
+ ]
+ },
+ {
+ rawValue : "dogs",
+ boundingBox: { x: 2.0, y: 2.0, width: 50.0, height: 50.0 },
+ cornerPoints: [
+ { x: 2.0, y: 2.0 },
+ { x: 52.0, y: 2.0 },
+ { x: 52.0, y: 52.0 },
+ { x: 2.0, y: 52.0 }
+ ]
+ },
+ ],
+ });
+ }
+
+ getFrameData() {
+ return this.bufferData_;
+ }
+
+ reset() {
+ this.receiver_.$.close();
+ this.interceptor_.stop();
+ }
+
+ }
+
+ let testInternal = {
+ initialized: false,
+ MockTextDetection: null
+ }
+
+ class TextDetectionTestChromium {
+ constructor() {
+ Object.freeze(this); // Make it immutable.
+ }
+
+ initialize() {
+ if (testInternal.initialized)
+ throw new Error('Call reset() before initialize().');
+
+ testInternal.MockTextDetection = new MockTextDetection;
+ testInternal.initialized = true;
+ }
+
+ // Resets state of text detection mocks between test runs.
+ async reset() {
+ if (!testInternal.initialized)
+ throw new Error('Call initialize() before reset().');
+ testInternal.MockTextDetection.reset();
+ testInternal.MockTextDetection = null;
+ testInternal.initialized = false;
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+
+ MockTextDetection() {
+ return testInternal.MockTextDetection;
+ }
+ }
+
+ return TextDetectionTestChromium;
+
+})();
diff --git a/testing/web-platform/tests/resources/chromium/mock-textdetection.js.headers b/testing/web-platform/tests/resources/chromium/mock-textdetection.js.headers
new file mode 100644
index 0000000000..6c61a34a4e
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/mock-textdetection.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8 \ No newline at end of file
diff --git a/testing/web-platform/tests/resources/chromium/nfc-mock.js b/testing/web-platform/tests/resources/chromium/nfc-mock.js
new file mode 100644
index 0000000000..31a71b9e22
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/nfc-mock.js
@@ -0,0 +1,437 @@
+import {NDEFErrorType, NDEFRecordTypeCategory, NFC, NFCReceiver} from '/gen/services/device/public/mojom/nfc.mojom.m.js';
+
+// Converts between NDEFMessageInit https://w3c.github.io/web-nfc/#dom-ndefmessage
+// and mojom.NDEFMessage structure, so that watch function can be tested.
+function toMojoNDEFMessage(message) {
+ let ndefMessage = {data: []};
+ for (let record of message.records) {
+ ndefMessage.data.push(toMojoNDEFRecord(record));
+ }
+ return ndefMessage;
+}
+
+function toMojoNDEFRecord(record) {
+ let nfcRecord = {};
+ // Simply checks the existence of ':' to decide whether it's an external
+ // type or a local type. As a mock, no need to really implement the validation
+ // algorithms for them.
+ if (record.recordType.startsWith(':')) {
+ nfcRecord.category = NDEFRecordTypeCategory.kLocal;
+ } else if (record.recordType.search(':') != -1) {
+ nfcRecord.category = NDEFRecordTypeCategory.kExternal;
+ } else {
+ nfcRecord.category = NDEFRecordTypeCategory.kStandardized;
+ }
+ nfcRecord.recordType = record.recordType;
+ nfcRecord.mediaType = record.mediaType;
+ nfcRecord.id = record.id;
+ if (record.recordType == 'text') {
+ nfcRecord.encoding = record.encoding == null? 'utf-8': record.encoding;
+ nfcRecord.lang = record.lang == null? 'en': record.lang;
+ }
+ nfcRecord.data = toByteArray(record.data);
+ if (record.data != null && record.data.records !== undefined) {
+ // |record.data| may be an NDEFMessageInit, i.e. the payload is a message.
+ nfcRecord.payloadMessage = toMojoNDEFMessage(record.data);
+ }
+ return nfcRecord;
+}
+
+// Converts JS objects to byte array.
+function toByteArray(data) {
+ if (data instanceof ArrayBuffer)
+ return new Uint8Array(data);
+ else if (ArrayBuffer.isView(data))
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
+
+ let byteArray = new Uint8Array(0);
+ let tmpData = data;
+ if (typeof tmpData === 'object' || typeof tmpData === 'number')
+ tmpData = JSON.stringify(tmpData);
+
+ if (typeof tmpData === 'string')
+ byteArray = new TextEncoder().encode(tmpData);
+
+ return byteArray;
+}
+
+// Compares NDEFRecords that were provided / received by the mock service.
+// TODO: Use different getters to get received record data,
+// see spec changes at https://github.com/w3c/web-nfc/pull/243.
+self.compareNDEFRecords = function(providedRecord, receivedRecord) {
+ assert_equals(providedRecord.recordType, receivedRecord.recordType);
+
+ if (providedRecord.id === undefined) {
+ assert_equals(null, receivedRecord.id);
+ } else {
+ assert_equals(providedRecord.id, receivedRecord.id);
+ }
+
+ if (providedRecord.mediaType === undefined) {
+ assert_equals(null, receivedRecord.mediaType);
+ } else {
+ assert_equals(providedRecord.mediaType, receivedRecord.mediaType);
+ }
+
+ assert_not_equals(providedRecord.recordType, 'empty');
+
+ if (providedRecord.recordType == 'text') {
+ assert_equals(
+ providedRecord.encoding == null? 'utf-8': providedRecord.encoding,
+ receivedRecord.encoding);
+ assert_equals(providedRecord.lang == null? 'en': providedRecord.lang,
+ receivedRecord.lang);
+ } else {
+ assert_equals(null, receivedRecord.encoding);
+ assert_equals(null, receivedRecord.lang);
+ }
+
+ assert_array_equals(toByteArray(providedRecord.data),
+ new Uint8Array(receivedRecord.data));
+}
+
+// Compares NDEFWriteOptions structures that were provided to API and
+// received by the mock mojo service.
+self.assertNDEFWriteOptionsEqual = function(provided, received) {
+ if (provided.overwrite !== undefined)
+ assert_equals(provided.overwrite, !!received.overwrite);
+ else
+ assert_equals(!!received.overwrite, true);
+}
+
+// Compares NDEFReaderOptions structures that were provided to API and
+// received by the mock mojo service.
+self.assertNDEFReaderOptionsEqual = function(provided, received) {
+ if (provided.url !== undefined)
+ assert_equals(provided.url, received.url);
+ else
+ assert_equals(received.url, '');
+
+ if (provided.mediaType !== undefined)
+ assert_equals(provided.mediaType, received.mediaType);
+ else
+ assert_equals(received.mediaType, '');
+
+ if (provided.recordType !== undefined) {
+ assert_equals(provided.recordType, received.recordType);
+ }
+}
+
+function createNDEFError(type) {
+ return {error: (type != null ? {errorType: type, errorMessage: ''} : null)};
+}
+
+self.WebNFCTest = (() => {
+ class MockNFC {
+ constructor() {
+ this.receiver_ = new NFCReceiver(this);
+
+ this.interceptor_ = new MojoInterfaceInterceptor(NFC.$interfaceName);
+ this.interceptor_.oninterfacerequest = e => {
+ if (this.should_close_pipe_on_request_)
+ e.handle.close();
+ else
+ this.receiver_.$.bindHandle(e.handle);
+ }
+
+ this.interceptor_.start();
+
+ this.hw_status_ = NFCHWStatus.ENABLED;
+ this.pushed_message_ = null;
+ this.pending_write_options_ = null;
+ this.pending_push_promise_func_ = null;
+ this.push_completed_ = true;
+ this.pending_make_read_only_promise_func_ = null;
+ this.make_read_only_completed_ = true;
+ this.client_ = null;
+ this.watchers_ = [];
+ this.reading_messages_ = [];
+ this.operations_suspended_ = false;
+ this.is_formatted_tag_ = false;
+ this.data_transfer_failed_ = false;
+ this.should_close_pipe_on_request_ = false;
+ }
+
+ // NFC delegate functions.
+ async push(message, options) {
+ const error = this.getHWError();
+ if (error)
+ return error;
+ // Cancels previous pending push operation.
+ if (this.pending_push_promise_func_) {
+ this.cancelPendingPushOperation();
+ }
+
+ this.pushed_message_ = message;
+ this.pending_write_options_ = options;
+ return new Promise(resolve => {
+ if (this.operations_suspended_ || !this.push_completed_) {
+ // Leaves the push pending.
+ this.pending_push_promise_func_ = resolve;
+ } else if (this.is_formatted_tag_ && !options.overwrite) {
+ // Resolves with NotAllowedError if there are NDEF records on the device
+ // and overwrite is false.
+ resolve(createNDEFError(NDEFErrorType.NOT_ALLOWED));
+ } else if (this.data_transfer_failed_) {
+ // Resolves with NetworkError if data transfer fails.
+ resolve(createNDEFError(NDEFErrorType.IO_ERROR));
+ } else {
+ resolve(createNDEFError(null));
+ }
+ });
+ }
+
+ async cancelPush() {
+ this.cancelPendingPushOperation();
+ return createNDEFError(null);
+ }
+
+ async makeReadOnly(options) {
+ const error = this.getHWError();
+ if (error)
+ return error;
+ // Cancels previous pending makeReadOnly operation.
+ if (this.pending_make_read_only_promise_func_) {
+ this.cancelPendingMakeReadOnlyOperation();
+ }
+
+ if (this.operations_suspended_ || !this.make_read_only_completed_) {
+ // Leaves the makeReadOnly pending.
+ return new Promise(resolve => {
+ this.pending_make_read_only_promise_func_ = resolve;
+ });
+ } else if (this.data_transfer_failed_) {
+ // Resolves with NetworkError if data transfer fails.
+ return createNDEFError(NDEFErrorType.IO_ERROR);
+ } else {
+ return createNDEFError(null);
+ }
+ }
+
+ async cancelMakeReadOnly() {
+ this.cancelPendingMakeReadOnlyOperation();
+ return createNDEFError(null);
+ }
+
+ setClient(client) {
+ this.client_ = client;
+ }
+
+ async watch(id) {
+ assert_true(id > 0);
+ const error = this.getHWError();
+ if (error) {
+ return error;
+ }
+
+ this.watchers_.push({id: id});
+ // Ignores reading if NFC operation is suspended
+ // or the NFC tag does not expose NDEF technology.
+ if (!this.operations_suspended_) {
+ // Triggers onWatch if the new watcher matches existing messages.
+ for (let message of this.reading_messages_) {
+ this.client_.onWatch(
+ [id], fake_tag_serial_number, toMojoNDEFMessage(message));
+ }
+ }
+
+ return createNDEFError(null);
+ }
+
+ cancelWatch(id) {
+ let index = this.watchers_.findIndex(value => value.id === id);
+ if (index !== -1) {
+ this.watchers_.splice(index, 1);
+ }
+ }
+
+ getHWError() {
+ if (this.hw_status_ === NFCHWStatus.DISABLED)
+ return createNDEFError(NDEFErrorType.NOT_READABLE);
+ if (this.hw_status_ === NFCHWStatus.NOT_SUPPORTED)
+ return createNDEFError(NDEFErrorType.NOT_SUPPORTED);
+ return null;
+ }
+
+ setHWStatus(status) {
+ this.hw_status_ = status;
+ }
+
+ pushedMessage() {
+ return this.pushed_message_;
+ }
+
+ writeOptions() {
+ return this.pending_write_options_;
+ }
+
+ watchOptions() {
+ assert_not_equals(this.watchers_.length, 0);
+ return this.watchers_[this.watchers_.length - 1].options;
+ }
+
+ setPendingPushCompleted(result) {
+ this.push_completed_ = result;
+ }
+
+ setPendingMakeReadOnlyCompleted(result) {
+ this.make_read_only_completed_ = result;
+ }
+
+ reset() {
+ this.hw_status_ = NFCHWStatus.ENABLED;
+ this.watchers_ = [];
+ this.reading_messages_ = [];
+ this.operations_suspended_ = false;
+ this.cancelPendingPushOperation();
+ this.cancelPendingMakeReadOnlyOperation();
+ this.is_formatted_tag_ = false;
+ this.data_transfer_failed_ = false;
+ this.should_close_pipe_on_request_ = false;
+ }
+
+ cancelPendingPushOperation() {
+ if (this.pending_push_promise_func_) {
+ this.pending_push_promise_func_(
+ createNDEFError(NDEFErrorType.OPERATION_CANCELLED));
+ this.pending_push_promise_func_ = null;
+ }
+
+ this.pushed_message_ = null;
+ this.pending_write_options_ = null;
+ this.push_completed_ = true;
+ }
+
+ cancelPendingMakeReadOnlyOperation() {
+ if (this.pending_make_read_only_promise_func_) {
+ this.pending_make_read_only_promise_func_(
+ createNDEFError(NDEFErrorType.OPERATION_CANCELLED));
+ this.pending_make_read_only_promise_func_ = null;
+ }
+
+ this.make_read_only_completed_ = true;
+ }
+
+ // Sets message that is used to deliver NFC reading updates.
+ setReadingMessage(message) {
+ this.reading_messages_.push(message);
+ // Ignores reading if NFC operation is suspended.
+ if(this.operations_suspended_) return;
+ // when overwrite is false, the write algorithm will read the NFC tag
+ // to determine if it has NDEF records on it.
+ if (this.pending_write_options_ && this.pending_write_options_.overwrite)
+ return;
+ // Triggers onWatch if the new message matches existing watchers.
+ for (let watcher of this.watchers_) {
+ this.client_.onWatch(
+ [watcher.id], fake_tag_serial_number,
+ toMojoNDEFMessage(message));
+ }
+ }
+
+ // Suspends all pending NFC operations. Could be used when web page
+ // visibility is lost.
+ suspendNFCOperations() {
+ this.operations_suspended_ = true;
+ }
+
+ // Resumes all suspended NFC operations.
+ resumeNFCOperations() {
+ this.operations_suspended_ = false;
+ // Resumes pending NFC reading.
+ for (let watcher of this.watchers_) {
+ for (let message of this.reading_messages_) {
+ this.client_.onWatch(
+ [watcher.id], fake_tag_serial_number,
+ toMojoNDEFMessage(message));
+ }
+ }
+ // Resumes pending push operation.
+ if (this.pending_push_promise_func_ && this.push_completed_) {
+ this.pending_push_promise_func_(createNDEFError(null));
+ this.pending_push_promise_func_ = null;
+ }
+ // Resumes pending makeReadOnly operation.
+ if (this.pending_make_read_only_promise_func_ &&
+ this.make_read_only_completed_) {
+ this.pending_make_read_only_promise_func_(createNDEFError(null));
+ this.pending_make_read_only_promise_func_ = null;
+ }
+ }
+
+ // Simulates the device coming in proximity does not expose NDEF technology.
+ simulateNonNDEFTagDiscovered() {
+ // Notify NotSupportedError to all active readers.
+ if (this.watchers_.length != 0) {
+ this.client_.onError({
+ errorType: NDEFErrorType.NOT_SUPPORTED,
+ errorMessage: ''
+ });
+ }
+ // Reject the pending push with NotSupportedError.
+ if (this.pending_push_promise_func_) {
+ this.pending_push_promise_func_(
+ createNDEFError(NDEFErrorType.NOT_SUPPORTED));
+ this.pending_push_promise_func_ = null;
+ }
+ // Reject the pending makeReadOnly with NotSupportedError.
+ if (this.pending_make_read_only_promise_func_) {
+ this.pending_make_read_only_promise_func_(
+ createNDEFError(NDEFErrorType.NOT_SUPPORTED));
+ this.pending_make_read_only_promise_func_ = null;
+ }
+ }
+
+ setIsFormattedTag(isFormatted) {
+ this.is_formatted_tag_ = isFormatted;
+ }
+
+ simulateDataTransferFails() {
+ this.data_transfer_failed_ = true;
+ }
+
+ simulateClosedPipe() {
+ this.should_close_pipe_on_request_ = true;
+ }
+ }
+
+ let testInternal = {
+ initialized: false,
+ mockNFC: null
+ }
+
+ class NFCTestChromium {
+ constructor() {
+ Object.freeze(this); // Makes it immutable.
+ }
+
+ async initialize() {
+ if (testInternal.initialized)
+ throw new Error('Call reset() before initialize().');
+
+ // Grant nfc permissions for Chromium testdriver.
+ await test_driver.set_permission({ name: 'nfc' }, 'granted');
+
+ if (testInternal.mockNFC == null) {
+ testInternal.mockNFC = new MockNFC();
+ }
+ testInternal.initialized = true;
+ }
+
+ // Reuses the nfc mock but resets its state between test runs.
+ async reset() {
+ if (!testInternal.initialized)
+ throw new Error('Call initialize() before reset().');
+ testInternal.mockNFC.reset();
+ testInternal.initialized = false;
+
+ await new Promise(resolve => setTimeout(resolve, 0));
+ }
+
+ getMockNFC() {
+ return testInternal.mockNFC;
+ }
+ }
+
+ return NFCTestChromium;
+})();
diff --git a/testing/web-platform/tests/resources/chromium/web-bluetooth-test.js b/testing/web-platform/tests/resources/chromium/web-bluetooth-test.js
new file mode 100644
index 0000000000..ecea5e760c
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/web-bluetooth-test.js
@@ -0,0 +1,629 @@
+'use strict';
+
+const content = {};
+const bluetooth = {};
+const MOJO_CHOOSER_EVENT_TYPE_MAP = {};
+
+function toMojoCentralState(state) {
+ switch (state) {
+ case 'absent':
+ return bluetooth.mojom.CentralState.ABSENT;
+ case 'powered-off':
+ return bluetooth.mojom.CentralState.POWERED_OFF;
+ case 'powered-on':
+ return bluetooth.mojom.CentralState.POWERED_ON;
+ default:
+ throw `Unsupported value ${state} for state.`;
+ }
+}
+
+// Converts bluetooth.mojom.WriteType to a string. If |writeType| is
+// invalid, this method will throw.
+function writeTypeToString(writeType) {
+ switch (writeType) {
+ case bluetooth.mojom.WriteType.kNone:
+ return 'none';
+ case bluetooth.mojom.WriteType.kWriteDefaultDeprecated:
+ return 'default-deprecated';
+ case bluetooth.mojom.WriteType.kWriteWithResponse:
+ return 'with-response';
+ case bluetooth.mojom.WriteType.kWriteWithoutResponse:
+ return 'without-response';
+ default:
+ throw `Unknown bluetooth.mojom.WriteType: ${writeType}`;
+ }
+}
+
+// Canonicalizes UUIDs and converts them to Mojo UUIDs.
+function canonicalizeAndConvertToMojoUUID(uuids) {
+ let canonicalUUIDs = uuids.map(val => ({uuid: BluetoothUUID.getService(val)}));
+ return canonicalUUIDs;
+}
+
+// Converts WebIDL a record<DOMString, BufferSource> to a map<K, array<uint8>> to
+// use for Mojo, where the value for K is calculated using keyFn.
+function convertToMojoMap(record, keyFn, isNumberKey = false) {
+ let map = new Map();
+ for (const [key, value] of Object.entries(record)) {
+ let buffer = ArrayBuffer.isView(value) ? value.buffer : value;
+ if (isNumberKey) {
+ let numberKey = parseInt(key);
+ if (Number.isNaN(numberKey))
+ throw `Map key ${key} is not a number`;
+ map.set(keyFn(numberKey), Array.from(new Uint8Array(buffer)));
+ continue;
+ }
+ map.set(keyFn(key), Array.from(new Uint8Array(buffer)));
+ }
+ return map;
+}
+
+function ArrayToMojoCharacteristicProperties(arr) {
+ const struct = {};
+ arr.forEach(property => { struct[property] = true; });
+ return struct;
+}
+
+class FakeBluetooth {
+ constructor() {
+ this.fake_bluetooth_ptr_ = new bluetooth.mojom.FakeBluetoothRemote();
+ this.fake_bluetooth_ptr_.$.bindNewPipeAndPassReceiver().bindInBrowser('process');
+ this.fake_central_ = null;
+ }
+
+ // Set it to indicate whether the platform supports BLE. For example,
+ // Windows 7 is a platform that doesn't support Low Energy. On the other
+ // hand Windows 10 is a platform that does support LE, even if there is no
+ // Bluetooth radio present.
+ async setLESupported(supported) {
+ if (typeof supported !== 'boolean') throw 'Type Not Supported';
+ await this.fake_bluetooth_ptr_.setLESupported(supported);
+ }
+
+ // Returns a promise that resolves with a FakeCentral that clients can use
+ // to simulate events that a device in the Central/Observer role would
+ // receive as well as monitor the operations performed by the device in the
+ // Central/Observer role.
+ // Calls sets LE as supported.
+ //
+ // A "Central" object would allow its clients to receive advertising events
+ // and initiate connections to peripherals i.e. operations of two roles
+ // defined by the Bluetooth Spec: Observer and Central.
+ // See Bluetooth 4.2 Vol 3 Part C 2.2.2 "Roles when Operating over an
+ // LE Physical Transport".
+ async simulateCentral({state}) {
+ if (this.fake_central_)
+ throw 'simulateCentral() should only be called once';
+
+ await this.setLESupported(true);
+
+ let {fakeCentral: fake_central_ptr} =
+ await this.fake_bluetooth_ptr_.simulateCentral(
+ toMojoCentralState(state));
+ this.fake_central_ = new FakeCentral(fake_central_ptr);
+ return this.fake_central_;
+ }
+
+ // Returns true if there are no pending responses.
+ async allResponsesConsumed() {
+ let {consumed} = await this.fake_bluetooth_ptr_.allResponsesConsumed();
+ return consumed;
+ }
+
+ // Returns a promise that resolves with a FakeChooser that clients can use to
+ // simulate chooser events.
+ async getManualChooser() {
+ if (typeof this.fake_chooser_ === 'undefined') {
+ this.fake_chooser_ = new FakeChooser();
+ }
+ return this.fake_chooser_;
+ }
+}
+
+// FakeCentral allows clients to simulate events that a device in the
+// Central/Observer role would receive as well as monitor the operations
+// performed by the device in the Central/Observer role.
+class FakeCentral {
+ constructor(fake_central_ptr) {
+ this.fake_central_ptr_ = fake_central_ptr;
+ this.peripherals_ = new Map();
+ }
+
+ // Simulates a peripheral with |address|, |name|, |manufacturerData| and
+ // |known_service_uuids| that has already been connected to the system. If the
+ // peripheral existed already it updates its name, manufacturer data, and
+ // known UUIDs. |known_service_uuids| should be an array of
+ // BluetoothServiceUUIDs
+ // https://webbluetoothcg.github.io/web-bluetooth/#typedefdef-bluetoothserviceuuid
+ //
+ // Platforms offer methods to retrieve devices that have already been
+ // connected to the system or weren't connected through the UA e.g. a user
+ // connected a peripheral through the system's settings. This method is
+ // intended to simulate peripherals that those methods would return.
+ async simulatePreconnectedPeripheral(
+ {address, name, manufacturerData = {}, knownServiceUUIDs = []}) {
+ await this.fake_central_ptr_.simulatePreconnectedPeripheral(
+ address, name,
+ convertToMojoMap(manufacturerData, Number, true /* isNumberKey */),
+ canonicalizeAndConvertToMojoUUID(knownServiceUUIDs));
+
+ return this.fetchOrCreatePeripheral_(address);
+ }
+
+ // Simulates an advertisement packet described by |scanResult| being received
+ // from a device. If central is currently scanning, the device will appear on
+ // the list of discovered devices.
+ async simulateAdvertisementReceived(scanResult) {
+ // Create a deep-copy to prevent the original |scanResult| from being
+ // modified when the UUIDs, manufacturer, and service data are converted.
+ let clonedScanResult = JSON.parse(JSON.stringify(scanResult));
+
+ if ('uuids' in scanResult.scanRecord) {
+ clonedScanResult.scanRecord.uuids =
+ canonicalizeAndConvertToMojoUUID(scanResult.scanRecord.uuids);
+ }
+
+ // Convert the optional appearance and txPower fields to the corresponding
+ // Mojo structures, since Mojo does not support optional interger values. If
+ // the fields are undefined, set the hasValue field as false and value as 0.
+ // Otherwise, set the hasValue field as true and value with the field value.
+ const has_appearance = 'appearance' in scanResult.scanRecord;
+ clonedScanResult.scanRecord.appearance = {
+ hasValue: has_appearance,
+ value: (has_appearance ? scanResult.scanRecord.appearance : 0)
+ }
+
+ const has_tx_power = 'txPower' in scanResult.scanRecord;
+ clonedScanResult.scanRecord.txPower = {
+ hasValue: has_tx_power,
+ value: (has_tx_power ? scanResult.scanRecord.txPower : 0)
+ }
+
+ // Convert manufacturerData from a record<DOMString, BufferSource> into a
+ // map<uint8, array<uint8>> for Mojo.
+ if ('manufacturerData' in scanResult.scanRecord) {
+ clonedScanResult.scanRecord.manufacturerData = convertToMojoMap(
+ scanResult.scanRecord.manufacturerData, Number,
+ true /* isNumberKey */);
+ }
+
+ // Convert serviceData from a record<DOMString, BufferSource> into a
+ // map<string, array<uint8>> for Mojo.
+ if ('serviceData' in scanResult.scanRecord) {
+ clonedScanResult.scanRecord.serviceData.serviceData = convertToMojoMap(
+ scanResult.scanRecord.serviceData, BluetoothUUID.getService,
+ false /* isNumberKey */);
+ }
+
+ await this.fake_central_ptr_.simulateAdvertisementReceived(
+ clonedScanResult);
+
+ return this.fetchOrCreatePeripheral_(clonedScanResult.deviceAddress);
+ }
+
+ // Simulates a change in the central device described by |state|. For example,
+ // setState('powered-off') can be used to simulate the central device powering
+ // off.
+ //
+ // This method should be used for any central state changes after
+ // simulateCentral() has been called to create a FakeCentral object.
+ async setState(state) {
+ await this.fake_central_ptr_.setState(toMojoCentralState(state));
+ }
+
+ // Create a fake_peripheral object from the given address.
+ fetchOrCreatePeripheral_(address) {
+ let peripheral = this.peripherals_.get(address);
+ if (peripheral === undefined) {
+ peripheral = new FakePeripheral(address, this.fake_central_ptr_);
+ this.peripherals_.set(address, peripheral);
+ }
+ return peripheral;
+ }
+}
+
+class FakePeripheral {
+ constructor(address, fake_central_ptr) {
+ this.address = address;
+ this.fake_central_ptr_ = fake_central_ptr;
+ }
+
+ // Adds a fake GATT Service with |uuid| to be discovered when discovering
+ // the peripheral's GATT Attributes. Returns a FakeRemoteGATTService
+ // corresponding to this service. |uuid| should be a BluetoothServiceUUIDs
+ // https://webbluetoothcg.github.io/web-bluetooth/#typedefdef-bluetoothserviceuuid
+ async addFakeService({uuid}) {
+ let {serviceId: service_id} = await this.fake_central_ptr_.addFakeService(
+ this.address, {uuid: BluetoothUUID.getService(uuid)});
+
+ if (service_id === null) throw 'addFakeService failed';
+
+ return new FakeRemoteGATTService(
+ service_id, this.address, this.fake_central_ptr_);
+ }
+
+ // Sets the next GATT Connection request response to |code|. |code| could be
+ // an HCI Error Code from BT 4.2 Vol 2 Part D 1.3 List Of Error Codes or a
+ // number outside that range returned by specific platforms e.g. Android
+ // returns 0x101 to signal a GATT failure
+ // https://developer.android.com/reference/android/bluetooth/BluetoothGatt.html#GATT_FAILURE
+ async setNextGATTConnectionResponse({code}) {
+ let {success} =
+ await this.fake_central_ptr_.setNextGATTConnectionResponse(
+ this.address, code);
+
+ if (success !== true) throw 'setNextGATTConnectionResponse failed.';
+ }
+
+ // Sets the next GATT Discovery request response for peripheral with
+ // |address| to |code|. |code| could be an HCI Error Code from
+ // BT 4.2 Vol 2 Part D 1.3 List Of Error Codes or a number outside that
+ // range returned by specific platforms e.g. Android returns 0x101 to signal
+ // a GATT failure
+ // https://developer.android.com/reference/android/bluetooth/BluetoothGatt.html#GATT_FAILURE
+ //
+ // The following procedures defined at BT 4.2 Vol 3 Part G Section 4.
+ // "GATT Feature Requirements" are used to discover attributes of the
+ // GATT Server:
+ // - Primary Service Discovery
+ // - Relationship Discovery
+ // - Characteristic Discovery
+ // - Characteristic Descriptor Discovery
+ // This method aims to simulate the response once all of these procedures
+ // have completed or if there was an error during any of them.
+ async setNextGATTDiscoveryResponse({code}) {
+ let {success} =
+ await this.fake_central_ptr_.setNextGATTDiscoveryResponse(
+ this.address, code);
+
+ if (success !== true) throw 'setNextGATTDiscoveryResponse failed.';
+ }
+
+ // Simulates a GATT disconnection from the peripheral with |address|.
+ async simulateGATTDisconnection() {
+ let {success} =
+ await this.fake_central_ptr_.simulateGATTDisconnection(this.address);
+
+ if (success !== true) throw 'simulateGATTDisconnection failed.';
+ }
+
+ // Simulates an Indication from the peripheral's GATT `Service Changed`
+ // Characteristic from BT 4.2 Vol 3 Part G 7.1. This Indication is signaled
+ // when services, characteristics, or descriptors are changed, added, or
+ // removed.
+ //
+ // The value for `Service Changed` is a range of attribute handles that have
+ // changed. However, this testing specification works at an abstracted
+ // level and does not expose setting attribute handles when adding
+ // attributes. Consequently, this simulate method should include the full
+ // range of all the peripheral's attribute handle values.
+ async simulateGATTServicesChanged() {
+ let {success} =
+ await this.fake_central_ptr_.simulateGATTServicesChanged(this.address);
+
+ if (success !== true) throw 'simulateGATTServicesChanged failed.';
+ }
+}
+
+class FakeRemoteGATTService {
+ constructor(service_id, peripheral_address, fake_central_ptr) {
+ this.service_id_ = service_id;
+ this.peripheral_address_ = peripheral_address;
+ this.fake_central_ptr_ = fake_central_ptr;
+ }
+
+ // Adds a fake GATT Characteristic with |uuid| and |properties|
+ // to this fake service. The characteristic will be found when discovering
+ // the peripheral's GATT Attributes. Returns a FakeRemoteGATTCharacteristic
+ // corresponding to the added characteristic.
+ async addFakeCharacteristic({uuid, properties}) {
+ let {characteristicId: characteristic_id} =
+ await this.fake_central_ptr_.addFakeCharacteristic(
+ {uuid: BluetoothUUID.getCharacteristic(uuid)},
+ ArrayToMojoCharacteristicProperties(properties),
+ this.service_id_,
+ this.peripheral_address_);
+
+ if (characteristic_id === null) throw 'addFakeCharacteristic failed';
+
+ return new FakeRemoteGATTCharacteristic(
+ characteristic_id, this.service_id_,
+ this.peripheral_address_, this.fake_central_ptr_);
+ }
+
+ // Removes the fake GATT service from its fake peripheral.
+ async remove() {
+ let {success} =
+ await this.fake_central_ptr_.removeFakeService(
+ this.service_id_,
+ this.peripheral_address_);
+
+ if (!success) throw 'remove failed';
+ }
+}
+
+class FakeRemoteGATTCharacteristic {
+ constructor(characteristic_id, service_id, peripheral_address,
+ fake_central_ptr) {
+ this.ids_ = [characteristic_id, service_id, peripheral_address];
+ this.descriptors_ = [];
+ this.fake_central_ptr_ = fake_central_ptr;
+ }
+
+ // Adds a fake GATT Descriptor with |uuid| to be discovered when
+ // discovering the peripheral's GATT Attributes. Returns a
+ // FakeRemoteGATTDescriptor corresponding to this descriptor. |uuid| should
+ // be a BluetoothDescriptorUUID
+ // https://webbluetoothcg.github.io/web-bluetooth/#typedefdef-bluetoothdescriptoruuid
+ async addFakeDescriptor({uuid}) {
+ let {descriptorId: descriptor_id} =
+ await this.fake_central_ptr_.addFakeDescriptor(
+ {uuid: BluetoothUUID.getDescriptor(uuid)}, ...this.ids_);
+
+ if (descriptor_id === null) throw 'addFakeDescriptor failed';
+
+ let fake_descriptor = new FakeRemoteGATTDescriptor(
+ descriptor_id, ...this.ids_, this.fake_central_ptr_);
+ this.descriptors_.push(fake_descriptor);
+
+ return fake_descriptor;
+ }
+
+ // Sets the next read response for characteristic to |code| and |value|.
+ // |code| could be a GATT Error Response from
+ // BT 4.2 Vol 3 Part F 3.4.1.1 Error Response or a number outside that range
+ // returned by specific platforms e.g. Android returns 0x101 to signal a GATT
+ // failure.
+ // https://developer.android.com/reference/android/bluetooth/BluetoothGatt.html#GATT_FAILURE
+ async setNextReadResponse(gatt_code, value=null) {
+ if (gatt_code === 0 && value === null) {
+ throw '|value| can\'t be null if read should success.';
+ }
+ if (gatt_code !== 0 && value !== null) {
+ throw '|value| must be null if read should fail.';
+ }
+
+ let {success} =
+ await this.fake_central_ptr_.setNextReadCharacteristicResponse(
+ gatt_code, value, ...this.ids_);
+
+ if (!success) throw 'setNextReadCharacteristicResponse failed';
+ }
+
+ // Sets the next write response for this characteristic to |code|. If
+ // writing to a characteristic that only supports 'write_without_response'
+ // the set response will be ignored.
+ // |code| could be a GATT Error Response from
+ // BT 4.2 Vol 3 Part F 3.4.1.1 Error Response or a number outside that range
+ // returned by specific platforms e.g. Android returns 0x101 to signal a GATT
+ // failure.
+ async setNextWriteResponse(gatt_code) {
+ let {success} =
+ await this.fake_central_ptr_.setNextWriteCharacteristicResponse(
+ gatt_code, ...this.ids_);
+
+ if (!success) throw 'setNextWriteCharacteristicResponse failed';
+ }
+
+ // Sets the next subscribe to notifications response for characteristic with
+ // |characteristic_id| in |service_id| and in |peripheral_address| to
+ // |code|. |code| could be a GATT Error Response from BT 4.2 Vol 3 Part F
+ // 3.4.1.1 Error Response or a number outside that range returned by
+ // specific platforms e.g. Android returns 0x101 to signal a GATT failure.
+ async setNextSubscribeToNotificationsResponse(gatt_code) {
+ let {success} =
+ await this.fake_central_ptr_.setNextSubscribeToNotificationsResponse(
+ gatt_code, ...this.ids_);
+
+ if (!success) throw 'setNextSubscribeToNotificationsResponse failed';
+ }
+
+ // Sets the next unsubscribe to notifications response for characteristic with
+ // |characteristic_id| in |service_id| and in |peripheral_address| to
+ // |code|. |code| could be a GATT Error Response from BT 4.2 Vol 3 Part F
+ // 3.4.1.1 Error Response or a number outside that range returned by
+ // specific platforms e.g. Android returns 0x101 to signal a GATT failure.
+ async setNextUnsubscribeFromNotificationsResponse(gatt_code) {
+ let {success} =
+ await this.fake_central_ptr_.setNextUnsubscribeFromNotificationsResponse(
+ gatt_code, ...this.ids_);
+
+ if (!success) throw 'setNextUnsubscribeToNotificationsResponse failed';
+ }
+
+ // Returns true if notifications from the characteristic have been subscribed
+ // to.
+ async isNotifying() {
+ let {success, isNotifying} =
+ await this.fake_central_ptr_.isNotifying(...this.ids_);
+
+ if (!success) throw 'isNotifying failed';
+
+ return isNotifying;
+ }
+
+ // Gets the last successfully written value to the characteristic and its
+ // write type. Write type is one of 'none', 'default-deprecated',
+ // 'with-response', 'without-response'. Returns {lastValue: null,
+ // lastWriteType: 'none'} if no value has yet been written to the
+ // characteristic.
+ async getLastWrittenValue() {
+ let {success, value, writeType} =
+ await this.fake_central_ptr_.getLastWrittenCharacteristicValue(
+ ...this.ids_);
+
+ if (!success) throw 'getLastWrittenCharacteristicValue failed';
+
+ return {lastValue: value, lastWriteType: writeTypeToString(writeType)};
+ }
+
+ // Removes the fake GATT Characteristic from its fake service.
+ async remove() {
+ let {success} =
+ await this.fake_central_ptr_.removeFakeCharacteristic(...this.ids_);
+
+ if (!success) throw 'remove failed';
+ }
+}
+
+class FakeRemoteGATTDescriptor {
+ constructor(descriptor_id,
+ characteristic_id,
+ service_id,
+ peripheral_address,
+ fake_central_ptr) {
+ this.ids_ = [
+ descriptor_id, characteristic_id, service_id, peripheral_address];
+ this.fake_central_ptr_ = fake_central_ptr;
+ }
+
+ // Sets the next read response for descriptor to |code| and |value|.
+ // |code| could be a GATT Error Response from
+ // BT 4.2 Vol 3 Part F 3.4.1.1 Error Response or a number outside that range
+ // returned by specific platforms e.g. Android returns 0x101 to signal a GATT
+ // failure.
+ // https://developer.android.com/reference/android/bluetooth/BluetoothGatt.html#GATT_FAILURE
+ async setNextReadResponse(gatt_code, value=null) {
+ if (gatt_code === 0 && value === null) {
+ throw '|value| cannot be null if read should succeed.';
+ }
+ if (gatt_code !== 0 && value !== null) {
+ throw '|value| must be null if read should fail.';
+ }
+
+ let {success} =
+ await this.fake_central_ptr_.setNextReadDescriptorResponse(
+ gatt_code, value, ...this.ids_);
+
+ if (!success) throw 'setNextReadDescriptorResponse failed';
+ }
+
+ // Sets the next write response for this descriptor to |code|.
+ // |code| could be a GATT Error Response from
+ // BT 4.2 Vol 3 Part F 3.4.1.1 Error Response or a number outside that range
+ // returned by specific platforms e.g. Android returns 0x101 to signal a GATT
+ // failure.
+ async setNextWriteResponse(gatt_code) {
+ let {success} =
+ await this.fake_central_ptr_.setNextWriteDescriptorResponse(
+ gatt_code, ...this.ids_);
+
+ if (!success) throw 'setNextWriteDescriptorResponse failed';
+ }
+
+ // Gets the last successfully written value to the descriptor.
+ // Returns null if no value has yet been written to the descriptor.
+ async getLastWrittenValue() {
+ let {success, value} =
+ await this.fake_central_ptr_.getLastWrittenDescriptorValue(
+ ...this.ids_);
+
+ if (!success) throw 'getLastWrittenDescriptorValue failed';
+
+ return value;
+ }
+
+ // Removes the fake GATT Descriptor from its fake characteristic.
+ async remove() {
+ let {success} =
+ await this.fake_central_ptr_.removeFakeDescriptor(...this.ids_);
+
+ if (!success) throw 'remove failed';
+ }
+}
+
+// FakeChooser allows clients to simulate user actions on a Bluetooth chooser,
+// and records the events produced by the Bluetooth chooser.
+class FakeChooser {
+ constructor() {
+ let fakeBluetoothChooserFactoryRemote =
+ new content.mojom.FakeBluetoothChooserFactoryRemote();
+ fakeBluetoothChooserFactoryRemote.$.bindNewPipeAndPassReceiver().bindInBrowser('process');
+
+ this.fake_bluetooth_chooser_ptr_ =
+ new content.mojom.FakeBluetoothChooserRemote();
+ this.fake_bluetooth_chooser_client_receiver_ =
+ new content.mojom.FakeBluetoothChooserClientReceiver(this);
+ fakeBluetoothChooserFactoryRemote.createFakeBluetoothChooser(
+ this.fake_bluetooth_chooser_ptr_.$.bindNewPipeAndPassReceiver(),
+ this.fake_bluetooth_chooser_client_receiver_.$.associateAndPassRemote());
+
+ this.events_ = new Array();
+ this.event_listener_ = null;
+ }
+
+ // If the chooser has received more events than |numOfEvents| this function
+ // will reject the promise, else it will wait until |numOfEvents| events are
+ // received before resolving with an array of |FakeBluetoothChooserEvent|
+ // objects.
+ async waitForEvents(numOfEvents) {
+ return new Promise(resolve => {
+ if (this.events_.length > numOfEvents) {
+ throw `Asked for ${numOfEvents} event(s), but received ` +
+ `${this.events_.length}.`;
+ }
+
+ this.event_listener_ = () => {
+ if (this.events_.length === numOfEvents) {
+ let result = Array.from(this.events_);
+ this.event_listener_ = null;
+ this.events_ = [];
+ resolve(result);
+ }
+ };
+ this.event_listener_();
+ });
+ }
+
+ async selectPeripheral(peripheral) {
+ if (!(peripheral instanceof FakePeripheral)) {
+ throw '|peripheral| must be an instance of FakePeripheral';
+ }
+ await this.fake_bluetooth_chooser_ptr_.selectPeripheral(peripheral.address);
+ }
+
+ async cancel() {
+ await this.fake_bluetooth_chooser_ptr_.cancel();
+ }
+
+ async rescan() {
+ await this.fake_bluetooth_chooser_ptr_.rescan();
+ }
+
+ onEvent(chooserEvent) {
+ chooserEvent.type = MOJO_CHOOSER_EVENT_TYPE_MAP[chooserEvent.type];
+ this.events_.push(chooserEvent);
+ if (this.event_listener_ !== null) {
+ this.event_listener_();
+ }
+ }
+}
+
+async function initializeChromiumResources() {
+ content.mojom = await import(
+ '/gen/content/web_test/common/fake_bluetooth_chooser.mojom.m.js');
+ bluetooth.mojom = await import(
+ '/gen/device/bluetooth/public/mojom/test/fake_bluetooth.mojom.m.js');
+
+ const map = MOJO_CHOOSER_EVENT_TYPE_MAP;
+ const types = content.mojom.ChooserEventType;
+ map[types.CHOOSER_OPENED] = 'chooser-opened';
+ map[types.CHOOSER_CLOSED] = 'chooser-closed';
+ map[types.ADAPTER_REMOVED] = 'adapter-removed';
+ map[types.ADAPTER_DISABLED] = 'adapter-disabled';
+ map[types.ADAPTER_ENABLED] = 'adapter-enabled';
+ map[types.DISCOVERY_FAILED_TO_START] = 'discovery-failed-to-start';
+ map[types.DISCOVERING] = 'discovering';
+ map[types.DISCOVERY_IDLE] = 'discovery-idle';
+ map[types.ADD_OR_UPDATE_DEVICE] = 'add-or-update-device';
+
+ // If this line fails, it means that current environment does not support the
+ // Web Bluetooth Test API.
+ try {
+ navigator.bluetooth.test = new FakeBluetooth();
+ } catch {
+ throw 'Web Bluetooth Test API is not implemented on this ' +
+ 'environment. See the bluetooth README at ' +
+ 'https://github.com/web-platform-tests/wpt/blob/master/bluetooth/README.md#web-bluetooth-testing';
+ }
+}
diff --git a/testing/web-platform/tests/resources/chromium/web-bluetooth-test.js.headers b/testing/web-platform/tests/resources/chromium/web-bluetooth-test.js.headers
new file mode 100644
index 0000000000..6805c323df
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/web-bluetooth-test.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/testing/web-platform/tests/resources/chromium/webusb-child-test.js b/testing/web-platform/tests/resources/chromium/webusb-child-test.js
new file mode 100644
index 0000000000..21412f66b0
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/webusb-child-test.js
@@ -0,0 +1,47 @@
+'use strict';
+
+// This polyfill prepares a child context to be attached to a parent context.
+// The parent must call navigator.usb.test.attachToContext() to attach to the
+// child context.
+(() => {
+ if (this.constructor.name === 'DedicatedWorkerGlobalScope' ||
+ this !== window.top) {
+
+ // Run Chromium specific set up code.
+ if (typeof MojoInterfaceInterceptor !== 'undefined') {
+ let messageChannel = new MessageChannel();
+ messageChannel.port1.onmessage = async (messageEvent) => {
+ if (messageEvent.data.type === 'Attach') {
+ messageEvent.data.interfaces.forEach(interfaceName => {
+ let interfaceInterceptor =
+ new MojoInterfaceInterceptor(interfaceName);
+ interfaceInterceptor.oninterfacerequest =
+ e => messageChannel.port1.postMessage({
+ type: interfaceName,
+ handle: e.handle
+ }, [e.handle]);
+ interfaceInterceptor.start();
+ });
+
+ // Wait for a call to GetDevices() to ensure that the interface
+ // handles are forwarded to the parent context.
+ try {
+ await navigator.usb.getDevices();
+ } catch (e) {
+ // This can happen in case of, for example, testing usb disallowed
+ // iframe.
+ console.error(`getDevices() throws error: ${e.name}: ${e.message}`);
+ }
+
+ messageChannel.port1.postMessage({ type: 'Complete' });
+ }
+ };
+
+ let message = { type: 'ReadyForAttachment', port: messageChannel.port2 };
+ if (typeof Window !== 'undefined')
+ parent.postMessage(message, '*', [messageChannel.port2]);
+ else
+ postMessage(message, [messageChannel.port2]);
+ }
+ }
+})();
diff --git a/testing/web-platform/tests/resources/chromium/webusb-child-test.js.headers b/testing/web-platform/tests/resources/chromium/webusb-child-test.js.headers
new file mode 100644
index 0000000000..6805c323df
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/webusb-child-test.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/testing/web-platform/tests/resources/chromium/webusb-test.js b/testing/web-platform/tests/resources/chromium/webusb-test.js
new file mode 100644
index 0000000000..94ff1bcadd
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/webusb-test.js
@@ -0,0 +1,582 @@
+'use strict';
+
+// This polyfill library implements the WebUSB Test API as specified here:
+// https://wicg.github.io/webusb/test/
+
+(() => {
+
+// These variables are logically members of the USBTest class but are defined
+// here to hide them from being visible as fields of navigator.usb.test.
+let internal = {
+ intialized: false,
+
+ webUsbService: null,
+ webUsbServiceInterceptor: null,
+
+ messagePort: null,
+};
+
+let mojom = {};
+
+async function loadMojomDefinitions() {
+ const deviceMojom =
+ await import('/gen/services/device/public/mojom/usb_device.mojom.m.js');
+ const serviceMojom = await import(
+ '/gen/third_party/blink/public/mojom/usb/web_usb_service.mojom.m.js');
+ return {
+ ...deviceMojom,
+ ...serviceMojom,
+ };
+}
+
+function getMessagePort(target) {
+ return new Promise(resolve => {
+ target.addEventListener('message', messageEvent => {
+ if (messageEvent.data.type === 'ReadyForAttachment') {
+ if (internal.messagePort === null) {
+ internal.messagePort = messageEvent.data.port;
+ }
+ resolve();
+ }
+ }, {once: true});
+ });
+}
+
+// Converts an ECMAScript String object to an instance of
+// mojo_base.mojom.String16.
+function mojoString16ToString(string16) {
+ return String.fromCharCode.apply(null, string16.data);
+}
+
+// Converts an instance of mojo_base.mojom.String16 to an ECMAScript String.
+function stringToMojoString16(string) {
+ let array = new Array(string.length);
+ for (var i = 0; i < string.length; ++i) {
+ array[i] = string.charCodeAt(i);
+ }
+ return { data: array }
+}
+
+function fakeDeviceInitToDeviceInfo(guid, init) {
+ let deviceInfo = {
+ guid: guid + "",
+ usbVersionMajor: init.usbVersionMajor,
+ usbVersionMinor: init.usbVersionMinor,
+ usbVersionSubminor: init.usbVersionSubminor,
+ classCode: init.deviceClass,
+ subclassCode: init.deviceSubclass,
+ protocolCode: init.deviceProtocol,
+ vendorId: init.vendorId,
+ productId: init.productId,
+ deviceVersionMajor: init.deviceVersionMajor,
+ deviceVersionMinor: init.deviceVersionMinor,
+ deviceVersionSubminor: init.deviceVersionSubminor,
+ manufacturerName: stringToMojoString16(init.manufacturerName),
+ productName: stringToMojoString16(init.productName),
+ serialNumber: stringToMojoString16(init.serialNumber),
+ activeConfiguration: init.activeConfigurationValue,
+ configurations: []
+ };
+ init.configurations.forEach(config => {
+ var configInfo = {
+ configurationValue: config.configurationValue,
+ configurationName: stringToMojoString16(config.configurationName),
+ selfPowered: false,
+ remoteWakeup: false,
+ maximumPower: 0,
+ interfaces: [],
+ extraData: new Uint8Array()
+ };
+ config.interfaces.forEach(iface => {
+ var interfaceInfo = {
+ interfaceNumber: iface.interfaceNumber,
+ alternates: []
+ };
+ iface.alternates.forEach(alternate => {
+ var alternateInfo = {
+ alternateSetting: alternate.alternateSetting,
+ classCode: alternate.interfaceClass,
+ subclassCode: alternate.interfaceSubclass,
+ protocolCode: alternate.interfaceProtocol,
+ interfaceName: stringToMojoString16(alternate.interfaceName),
+ endpoints: [],
+ extraData: new Uint8Array()
+ };
+ alternate.endpoints.forEach(endpoint => {
+ var endpointInfo = {
+ endpointNumber: endpoint.endpointNumber,
+ packetSize: endpoint.packetSize,
+ synchronizationType: mojom.UsbSynchronizationType.NONE,
+ usageType: mojom.UsbUsageType.DATA,
+ pollingInterval: 0,
+ extraData: new Uint8Array()
+ };
+ switch (endpoint.direction) {
+ case "in":
+ endpointInfo.direction = mojom.UsbTransferDirection.INBOUND;
+ break;
+ case "out":
+ endpointInfo.direction = mojom.UsbTransferDirection.OUTBOUND;
+ break;
+ }
+ switch (endpoint.type) {
+ case "bulk":
+ endpointInfo.type = mojom.UsbTransferType.BULK;
+ break;
+ case "interrupt":
+ endpointInfo.type = mojom.UsbTransferType.INTERRUPT;
+ break;
+ case "isochronous":
+ endpointInfo.type = mojom.UsbTransferType.ISOCHRONOUS;
+ break;
+ }
+ alternateInfo.endpoints.push(endpointInfo);
+ });
+ interfaceInfo.alternates.push(alternateInfo);
+ });
+ configInfo.interfaces.push(interfaceInfo);
+ });
+ deviceInfo.configurations.push(configInfo);
+ });
+ return deviceInfo;
+}
+
+function convertMojoDeviceFilters(input) {
+ let output = [];
+ input.forEach(filter => {
+ output.push(convertMojoDeviceFilter(filter));
+ });
+ return output;
+}
+
+function convertMojoDeviceFilter(input) {
+ let output = {};
+ if (input.hasVendorId)
+ output.vendorId = input.vendorId;
+ if (input.hasProductId)
+ output.productId = input.productId;
+ if (input.hasClassCode)
+ output.classCode = input.classCode;
+ if (input.hasSubclassCode)
+ output.subclassCode = input.subclassCode;
+ if (input.hasProtocolCode)
+ output.protocolCode = input.protocolCode;
+ if (input.serialNumber)
+ output.serialNumber = mojoString16ToString(input.serialNumber);
+ return output;
+}
+
+class FakeDevice {
+ constructor(deviceInit) {
+ this.info_ = deviceInit;
+ this.opened_ = false;
+ this.currentConfiguration_ = null;
+ this.claimedInterfaces_ = new Map();
+ }
+
+ getConfiguration() {
+ if (this.currentConfiguration_) {
+ return Promise.resolve({
+ value: this.currentConfiguration_.configurationValue });
+ } else {
+ return Promise.resolve({ value: 0 });
+ }
+ }
+
+ open() {
+ assert_false(this.opened_);
+ this.opened_ = true;
+ return Promise.resolve({result: {success: mojom.UsbOpenDeviceSuccess.OK}});
+ }
+
+ close() {
+ assert_true(this.opened_);
+ this.opened_ = false;
+ return Promise.resolve();
+ }
+
+ setConfiguration(value) {
+ assert_true(this.opened_);
+
+ let selectedConfiguration = this.info_.configurations.find(
+ configuration => configuration.configurationValue == value);
+ // Blink should never request an invalid configuration.
+ assert_not_equals(selectedConfiguration, undefined);
+ this.currentConfiguration_ = selectedConfiguration;
+ return Promise.resolve({ success: true });
+ }
+
+ async claimInterface(interfaceNumber) {
+ assert_true(this.opened_);
+ assert_false(this.currentConfiguration_ == null, 'device configured');
+ assert_false(this.claimedInterfaces_.has(interfaceNumber),
+ 'interface already claimed');
+
+ const protectedInterfaces = new Set([
+ mojom.USB_AUDIO_CLASS,
+ mojom.USB_HID_CLASS,
+ mojom.USB_MASS_STORAGE_CLASS,
+ mojom.USB_SMART_CARD_CLASS,
+ mojom.USB_VIDEO_CLASS,
+ mojom.USB_AUDIO_VIDEO_CLASS,
+ mojom.USB_WIRELESS_CLASS,
+ ]);
+
+ let iface = this.currentConfiguration_.interfaces.find(
+ iface => iface.interfaceNumber == interfaceNumber);
+ // Blink should never request an invalid interface or alternate.
+ assert_false(iface == undefined);
+ if (iface.alternates.some(
+ alt => protectedInterfaces.has(alt.interfaceClass))) {
+ return {result: mojom.UsbClaimInterfaceResult.kProtectedClass};
+ }
+
+ this.claimedInterfaces_.set(interfaceNumber, 0);
+ return {result: mojom.UsbClaimInterfaceResult.kSuccess};
+ }
+
+ releaseInterface(interfaceNumber) {
+ assert_true(this.opened_);
+ assert_false(this.currentConfiguration_ == null, 'device configured');
+ assert_true(this.claimedInterfaces_.has(interfaceNumber));
+ this.claimedInterfaces_.delete(interfaceNumber);
+ return Promise.resolve({ success: true });
+ }
+
+ setInterfaceAlternateSetting(interfaceNumber, alternateSetting) {
+ assert_true(this.opened_);
+ assert_false(this.currentConfiguration_ == null, 'device configured');
+ assert_true(this.claimedInterfaces_.has(interfaceNumber));
+
+ let iface = this.currentConfiguration_.interfaces.find(
+ iface => iface.interfaceNumber == interfaceNumber);
+ // Blink should never request an invalid interface or alternate.
+ assert_false(iface == undefined);
+ assert_true(iface.alternates.some(
+ x => x.alternateSetting == alternateSetting));
+ this.claimedInterfaces_.set(interfaceNumber, alternateSetting);
+ return Promise.resolve({ success: true });
+ }
+
+ reset() {
+ assert_true(this.opened_);
+ return Promise.resolve({ success: true });
+ }
+
+ clearHalt(endpoint) {
+ assert_true(this.opened_);
+ assert_false(this.currentConfiguration_ == null, 'device configured');
+ // TODO(reillyg): Assert that endpoint is valid.
+ return Promise.resolve({ success: true });
+ }
+
+ async controlTransferIn(params, length, timeout) {
+ assert_true(this.opened_);
+
+ if ((params.recipient == mojom.UsbControlTransferRecipient.INTERFACE ||
+ params.recipient == mojom.UsbControlTransferRecipient.ENDPOINT) &&
+ this.currentConfiguration_ == null) {
+ return {
+ status: mojom.UsbTransferStatus.PERMISSION_DENIED,
+ };
+ }
+
+ return {
+ status: mojom.UsbTransferStatus.OK,
+ data: {
+ buffer: [
+ length >> 8, length & 0xff, params.request, params.value >> 8,
+ params.value & 0xff, params.index >> 8, params.index & 0xff
+ ]
+ }
+ };
+ }
+
+ async controlTransferOut(params, data, timeout) {
+ assert_true(this.opened_);
+
+ if ((params.recipient == mojom.UsbControlTransferRecipient.INTERFACE ||
+ params.recipient == mojom.UsbControlTransferRecipient.ENDPOINT) &&
+ this.currentConfiguration_ == null) {
+ return {
+ status: mojom.UsbTransferStatus.PERMISSION_DENIED,
+ };
+ }
+
+ return {status: mojom.UsbTransferStatus.OK, bytesWritten: data.byteLength};
+ }
+
+ genericTransferIn(endpointNumber, length, timeout) {
+ assert_true(this.opened_);
+ assert_false(this.currentConfiguration_ == null, 'device configured');
+ // TODO(reillyg): Assert that endpoint is valid.
+ let data = new Array(length);
+ for (let i = 0; i < length; ++i)
+ data[i] = i & 0xff;
+ return Promise.resolve(
+ {status: mojom.UsbTransferStatus.OK, data: {buffer: data}});
+ }
+
+ genericTransferOut(endpointNumber, data, timeout) {
+ assert_true(this.opened_);
+ assert_false(this.currentConfiguration_ == null, 'device configured');
+ // TODO(reillyg): Assert that endpoint is valid.
+ return Promise.resolve(
+ {status: mojom.UsbTransferStatus.OK, bytesWritten: data.byteLength});
+ }
+
+ isochronousTransferIn(endpointNumber, packetLengths, timeout) {
+ assert_true(this.opened_);
+ assert_false(this.currentConfiguration_ == null, 'device configured');
+ // TODO(reillyg): Assert that endpoint is valid.
+ let data = new Array(packetLengths.reduce((a, b) => a + b, 0));
+ let dataOffset = 0;
+ let packets = new Array(packetLengths.length);
+ for (let i = 0; i < packetLengths.length; ++i) {
+ for (let j = 0; j < packetLengths[i]; ++j)
+ data[dataOffset++] = j & 0xff;
+ packets[i] = {
+ length: packetLengths[i],
+ transferredLength: packetLengths[i],
+ status: mojom.UsbTransferStatus.OK
+ };
+ }
+ return Promise.resolve({data: {buffer: data}, packets: packets});
+ }
+
+ isochronousTransferOut(endpointNumber, data, packetLengths, timeout) {
+ assert_true(this.opened_);
+ assert_false(this.currentConfiguration_ == null, 'device configured');
+ // TODO(reillyg): Assert that endpoint is valid.
+ let packets = new Array(packetLengths.length);
+ for (let i = 0; i < packetLengths.length; ++i) {
+ packets[i] = {
+ length: packetLengths[i],
+ transferredLength: packetLengths[i],
+ status: mojom.UsbTransferStatus.OK
+ };
+ }
+ return Promise.resolve({ packets: packets });
+ }
+}
+
+class FakeWebUsbService {
+ constructor() {
+ this.receiver_ = new mojom.WebUsbServiceReceiver(this);
+ this.devices_ = new Map();
+ this.devicesByGuid_ = new Map();
+ this.client_ = null;
+ this.nextGuid_ = 0;
+ }
+
+ addBinding(handle) {
+ this.receiver_.$.bindHandle(handle);
+ }
+
+ addDevice(fakeDevice, info) {
+ let device = {
+ fakeDevice: fakeDevice,
+ guid: (this.nextGuid_++).toString(),
+ info: info,
+ receivers: [],
+ };
+ this.devices_.set(fakeDevice, device);
+ this.devicesByGuid_.set(device.guid, device);
+ if (this.client_)
+ this.client_.onDeviceAdded(fakeDeviceInitToDeviceInfo(device.guid, info));
+ }
+
+ async forgetDevice(guid) {
+ // Permissions are currently untestable through WPT.
+ }
+
+ removeDevice(fakeDevice) {
+ let device = this.devices_.get(fakeDevice);
+ if (!device)
+ throw new Error('Cannot remove unknown device.');
+
+ for (const receiver of device.receivers)
+ receiver.$.close();
+ this.devices_.delete(device.fakeDevice);
+ this.devicesByGuid_.delete(device.guid);
+ if (this.client_) {
+ this.client_.onDeviceRemoved(
+ fakeDeviceInitToDeviceInfo(device.guid, device.info));
+ }
+ }
+
+ removeAllDevices() {
+ this.devices_.forEach(device => {
+ for (const receiver of device.receivers)
+ receiver.$.close();
+ this.client_.onDeviceRemoved(
+ fakeDeviceInitToDeviceInfo(device.guid, device.info));
+ });
+ this.devices_.clear();
+ this.devicesByGuid_.clear();
+ }
+
+ getDevices() {
+ let devices = [];
+ this.devices_.forEach(device => {
+ devices.push(fakeDeviceInitToDeviceInfo(device.guid, device.info));
+ });
+ return Promise.resolve({ results: devices });
+ }
+
+ getDevice(guid, request) {
+ let retrievedDevice = this.devicesByGuid_.get(guid);
+ if (retrievedDevice) {
+ const receiver =
+ new mojom.UsbDeviceReceiver(new FakeDevice(retrievedDevice.info));
+ receiver.$.bindHandle(request.handle);
+ receiver.onConnectionError.addListener(() => {
+ if (retrievedDevice.fakeDevice.onclose)
+ retrievedDevice.fakeDevice.onclose();
+ });
+ retrievedDevice.receivers.push(receiver);
+ } else {
+ request.handle.close();
+ }
+ }
+
+ getPermission(deviceFilters) {
+ return new Promise(resolve => {
+ if (navigator.usb.test.onrequestdevice) {
+ navigator.usb.test.onrequestdevice(
+ new USBDeviceRequestEvent(deviceFilters, resolve));
+ } else {
+ resolve({ result: null });
+ }
+ });
+ }
+
+ setClient(client) {
+ this.client_ = client;
+ }
+}
+
+class USBDeviceRequestEvent {
+ constructor(deviceFilters, resolve) {
+ this.filters = convertMojoDeviceFilters(deviceFilters);
+ this.resolveFunc_ = resolve;
+ }
+
+ respondWith(value) {
+ // Wait until |value| resolves (if it is a Promise). This function returns
+ // no value.
+ Promise.resolve(value).then(fakeDevice => {
+ let device = internal.webUsbService.devices_.get(fakeDevice);
+ let result = null;
+ if (device) {
+ result = fakeDeviceInitToDeviceInfo(device.guid, device.info);
+ }
+ this.resolveFunc_({ result: result });
+ }, () => {
+ this.resolveFunc_({ result: null });
+ });
+ }
+}
+
+// Unlike FakeDevice this class is exported to callers of USBTest.addFakeDevice.
+class FakeUSBDevice {
+ constructor() {
+ this.onclose = null;
+ }
+
+ disconnect() {
+ setTimeout(() => internal.webUsbService.removeDevice(this), 0);
+ }
+}
+
+class USBTest {
+ constructor() {
+ this.onrequestdevice = undefined;
+ }
+
+ async initialize() {
+ if (internal.initialized)
+ return;
+
+ // Be ready to handle 'ReadyForAttachment' message from child iframes.
+ if ('window' in self) {
+ getMessagePort(window);
+ }
+
+ mojom = await loadMojomDefinitions();
+ internal.webUsbService = new FakeWebUsbService();
+ internal.webUsbServiceInterceptor =
+ new MojoInterfaceInterceptor(mojom.WebUsbService.$interfaceName);
+ internal.webUsbServiceInterceptor.oninterfacerequest =
+ e => internal.webUsbService.addBinding(e.handle);
+ internal.webUsbServiceInterceptor.start();
+
+ // Wait for a call to GetDevices() to pass between the renderer and the
+ // mock in order to establish that everything is set up.
+ await navigator.usb.getDevices();
+ internal.initialized = true;
+ }
+
+ // Returns a promise that is resolved when the implementation of |usb| in the
+ // global scope for |context| is controlled by the current context.
+ attachToContext(context) {
+ if (!internal.initialized)
+ throw new Error('Call initialize() before attachToContext()');
+
+ let target = context.constructor.name === 'Worker' ? context : window;
+ return getMessagePort(target).then(() => {
+ return new Promise(resolve => {
+ internal.messagePort.onmessage = channelEvent => {
+ switch (channelEvent.data.type) {
+ case mojom.WebUsbService.$interfaceName:
+ internal.webUsbService.addBinding(channelEvent.data.handle);
+ break;
+ case 'Complete':
+ resolve();
+ break;
+ }
+ };
+ internal.messagePort.postMessage({
+ type: 'Attach',
+ interfaces: [
+ mojom.WebUsbService.$interfaceName,
+ ]
+ });
+ });
+ });
+ }
+
+ addFakeDevice(deviceInit) {
+ if (!internal.initialized)
+ throw new Error('Call initialize() before addFakeDevice().');
+
+ // |addDevice| and |removeDevice| are called in a setTimeout callback so
+ // that tests do not rely on the device being immediately available which
+ // may not be true for all implementations of this test API.
+ let fakeDevice = new FakeUSBDevice();
+ setTimeout(
+ () => internal.webUsbService.addDevice(fakeDevice, deviceInit), 0);
+ return fakeDevice;
+ }
+
+ reset() {
+ if (!internal.initialized)
+ throw new Error('Call initialize() before reset().');
+
+ // Reset the mocks in a setTimeout callback so that tests do not rely on
+ // the fact that this polyfill can do this synchronously.
+ return new Promise(resolve => {
+ setTimeout(() => {
+ if (internal.messagePort !== null)
+ internal.messagePort.close();
+ internal.messagePort = null;
+ internal.webUsbService.removeAllDevices();
+ resolve();
+ }, 0);
+ });
+ }
+}
+
+navigator.usb.test = new USBTest();
+
+})();
diff --git a/testing/web-platform/tests/resources/chromium/webusb-test.js.headers b/testing/web-platform/tests/resources/chromium/webusb-test.js.headers
new file mode 100644
index 0000000000..6805c323df
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/webusb-test.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/testing/web-platform/tests/resources/chromium/webxr-test-math-helper.js b/testing/web-platform/tests/resources/chromium/webxr-test-math-helper.js
new file mode 100644
index 0000000000..22c6c12d08
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/webxr-test-math-helper.js
@@ -0,0 +1,298 @@
+'use strict';
+
+// Math helper - used mainly in hit test implementation done by webxr-test.js
+class XRMathHelper {
+ static toString(p) {
+ return "[" + p.x + "," + p.y + "," + p.z + "," + p.w + "]";
+ }
+
+ static transform_by_matrix(matrix, point) {
+ return {
+ x : matrix[0] * point.x + matrix[4] * point.y + matrix[8] * point.z + matrix[12] * point.w,
+ y : matrix[1] * point.x + matrix[5] * point.y + matrix[9] * point.z + matrix[13] * point.w,
+ z : matrix[2] * point.x + matrix[6] * point.y + matrix[10] * point.z + matrix[14] * point.w,
+ w : matrix[3] * point.x + matrix[7] * point.y + matrix[11] * point.z + matrix[15] * point.w,
+ };
+ }
+
+ static neg(p) {
+ return {x : -p.x, y : -p.y, z : -p.z, w : p.w};
+ }
+
+ static sub(lhs, rhs) {
+ // .w is treated here like an entity type, 1 signifies points, 0 signifies vectors.
+ // point - point, point - vector, vector - vector are ok, vector - point is not.
+ if (lhs.w != rhs.w && lhs.w == 0.0) {
+ throw new Error("vector - point not allowed: " + toString(lhs) + "-" + toString(rhs));
+ }
+
+ return {x : lhs.x - rhs.x, y : lhs.y - rhs.y, z : lhs.z - rhs.z, w : lhs.w - rhs.w};
+ }
+
+ static add(lhs, rhs) {
+ if (lhs.w == rhs.w && lhs.w == 1.0) {
+ throw new Error("point + point not allowed", p1, p2);
+ }
+
+ return {x : lhs.x + rhs.x, y : lhs.y + rhs.y, z : lhs.z + rhs.z, w : lhs.w + rhs.w};
+ }
+
+ static cross(lhs, rhs) {
+ if (lhs.w != 0.0 || rhs.w != 0.0) {
+ throw new Error("cross product not allowed: " + toString(lhs) + "x" + toString(rhs));
+ }
+
+ return {
+ x : lhs.y * rhs.z - lhs.z * rhs.y,
+ y : lhs.z * rhs.x - lhs.x * rhs.z,
+ z : lhs.x * rhs.y - lhs.y * rhs.x,
+ w : 0
+ };
+ }
+
+ static dot(lhs, rhs) {
+ if (lhs.w != 0 || rhs.w != 0) {
+ throw new Error("dot product not allowed: " + toString(lhs) + "x" + toString(rhs));
+ }
+
+ return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z;
+ }
+
+ static mul(scalar, vector) {
+ if (vector.w != 0) {
+ throw new Error("scalar * vector not allowed", scalar, vector);
+ }
+
+ return {x : vector.x * scalar, y : vector.y * scalar, z : vector.z * scalar, w : vector.w};
+ }
+
+ static length(vector) {
+ return Math.sqrt(XRMathHelper.dot(vector, vector));
+ }
+
+ static normalize(vector) {
+ const l = XRMathHelper.length(vector);
+ return XRMathHelper.mul(1.0/l, vector);
+ }
+
+ // All |face|'s points and |point| must be co-planar.
+ static pointInFace(point, face) {
+ const normalize = XRMathHelper.normalize;
+ const sub = XRMathHelper.sub;
+ const length = XRMathHelper.length;
+ const cross = XRMathHelper.cross;
+
+ let onTheRight = null;
+ let previous_point = face[face.length - 1];
+
+ // |point| is in |face| if it's on the same side of all the edges.
+ for (let i = 0; i < face.length; ++i) {
+ const current_point = face[i];
+
+ const edge_direction = normalize(sub(current_point, previous_point));
+ const turn_direction = normalize(sub(point, current_point));
+
+ const sin_turn_angle = length(cross(edge_direction, turn_direction));
+
+ if (onTheRight == null) {
+ onTheRight = sin_turn_angle >= 0;
+ } else {
+ if (onTheRight && sin_turn_angle < 0) return false;
+ if (!onTheRight && sin_turn_angle > 0) return false;
+ }
+
+ previous_point = current_point;
+ }
+
+ return true;
+ }
+
+ static det2x2(m00, m01, m10, m11) {
+ return m00 * m11 - m01 * m10;
+ }
+
+ static det3x3(
+ m00, m01, m02,
+ m10, m11, m12,
+ m20, m21, m22
+ ){
+ const det2x2 = XRMathHelper.det2x2;
+
+ return m00 * det2x2(m11, m12, m21, m22)
+ - m01 * det2x2(m10, m12, m20, m22)
+ + m02 * det2x2(m10, m11, m20, m21);
+ }
+
+ static det4x4(
+ m00, m01, m02, m03,
+ m10, m11, m12, m13,
+ m20, m21, m22, m23,
+ m30, m31, m32, m33
+ ) {
+ const det3x3 = XRMathHelper.det3x3;
+
+ return m00 * det3x3(m11, m12, m13,
+ m21, m22, m23,
+ m31, m32, m33)
+ - m01 * det3x3(m10, m12, m13,
+ m20, m22, m23,
+ m30, m32, m33)
+ + m02 * det3x3(m10, m11, m13,
+ m20, m21, m23,
+ m30, m31, m33)
+ - m03 * det3x3(m10, m11, m12,
+ m20, m21, m22,
+ m30, m31, m32);
+ }
+
+ static inv2(m) {
+ // mij - i-th column, j-th row
+ const m00 = m[0], m01 = m[1], m02 = m[2], m03 = m[3];
+ const m10 = m[4], m11 = m[5], m12 = m[6], m13 = m[7];
+ const m20 = m[8], m21 = m[9], m22 = m[10], m23 = m[11];
+ const m30 = m[12], m31 = m[13], m32 = m[14], m33 = m[15];
+
+ const det = det4x4(
+ m00, m01, m02, m03,
+ m10, m11, m12, m13,
+ m20, m21, m22, m23,
+ m30, m31, m32, m33
+ );
+ }
+
+ static transpose(m) {
+ const result = Array(16);
+ for (let i = 0; i < 4; i++) {
+ for (let j = 0; j < 4; j++) {
+ result[i * 4 + j] = m[j * 4 + i];
+ }
+ }
+ return result;
+ }
+
+ // Inverts the matrix, ported from transformation_matrix.cc.
+ static inverse(m) {
+ const det3x3 = XRMathHelper.det3x3;
+
+ // mij - i-th column, j-th row
+ const m00 = m[0], m01 = m[1], m02 = m[2], m03 = m[3];
+ const m10 = m[4], m11 = m[5], m12 = m[6], m13 = m[7];
+ const m20 = m[8], m21 = m[9], m22 = m[10], m23 = m[11];
+ const m30 = m[12], m31 = m[13], m32 = m[14], m33 = m[15];
+
+ const det = XRMathHelper.det4x4(
+ m00, m01, m02, m03,
+ m10, m11, m12, m13,
+ m20, m21, m22, m23,
+ m30, m31, m32, m33
+ );
+
+ if (Math.abs(det) < 0.0001) {
+ return null;
+ }
+
+ const invDet = 1.0 / det;
+ // Calculate `comatrix * 1/det`:
+ const result2 = [
+ // First column (m0r):
+ invDet * det3x3(m11, m12, m13, m21, m22, m23, m32, m32, m33),
+ -invDet * det3x3(m10, m12, m13, m20, m22, m23, m30, m32, m33),
+ invDet * det3x3(m10, m11, m13, m20, m21, m23, m30, m31, m33),
+ -invDet * det3x3(m10, m11, m12, m20, m21, m22, m30, m31, m32),
+ // Second column (m1r):
+ -invDet * det3x3(m01, m02, m03, m21, m22, m23, m32, m32, m33),
+ invDet * det3x3(m00, m02, m03, m20, m22, m23, m30, m32, m33),
+ -invDet * det3x3(m00, m01, m03, m20, m21, m23, m30, m31, m33),
+ invDet * det3x3(m00, m01, m02, m20, m21, m22, m30, m31, m32),
+ // Third column (m2r):
+ invDet * det3x3(m01, m02, m03, m11, m12, m13, m31, m32, m33),
+ -invDet * det3x3(m00, m02, m03, m10, m12, m13, m30, m32, m33),
+ invDet * det3x3(m00, m01, m03, m10, m11, m13, m30, m31, m33),
+ -invDet * det3x3(m00, m01, m02, m10, m11, m12, m30, m31, m32),
+ // Fourth column (m3r):
+ -invDet * det3x3(m01, m02, m03, m11, m12, m13, m21, m22, m23),
+ invDet * det3x3(m00, m02, m03, m10, m12, m13, m20, m22, m23),
+ -invDet * det3x3(m00, m01, m03, m10, m11, m13, m20, m21, m23),
+ invDet * det3x3(m00, m01, m02, m10, m11, m12, m20, m21, m22),
+ ];
+
+ // Actual inverse is `1/det * transposed(comatrix)`:
+ return XRMathHelper.transpose(result2);
+ }
+
+ static mul4x4(m1, m2) {
+ if (m1 == null || m2 == null) {
+ return null;
+ }
+
+ const result = Array(16);
+
+ for (let row = 0; row < 4; row++) {
+ for (let col = 0; col < 4; col++) {
+ result[4 * col + row] = 0;
+ for(let i = 0; i < 4; i++) {
+ result[4 * col + row] += m1[4 * i + row] * m2[4 * col + i];
+ }
+ }
+ }
+
+ return result;
+ }
+
+ // Decomposes a matrix, with the assumption that the passed in matrix is
+ // a rigid transformation (i.e. position and rotation *only*!).
+ // The result is an object with `position` and `orientation` keys, which should
+ // be compatible with FakeXRRigidTransformInit.
+ // The implementation should match the behavior of gfx::Transform, but assumes
+ // that scale, skew & perspective are not present in the matrix so it could be
+ // simplified.
+ static decomposeRigidTransform(m) {
+ const m00 = m[0], m01 = m[1], m02 = m[2], m03 = m[3];
+ const m10 = m[4], m11 = m[5], m12 = m[6], m13 = m[7];
+ const m20 = m[8], m21 = m[9], m22 = m[10], m23 = m[11];
+ const m30 = m[12], m31 = m[13], m32 = m[14], m33 = m[15];
+
+ const position = [m30, m31, m32];
+ const orientation = [0, 0, 0, 0];
+
+ const trace = m00 + m11 + m22;
+ if (trace > 0) {
+ const S = Math.sqrt(trace + 1) * 2;
+ orientation[3] = 0.25 * S;
+ orientation[0] = (m12 - m21) / S;
+ orientation[1] = (m20 - m02) / S;
+ orientation[2] = (m01 - m10) / S;
+ } else if (m00 > m11 && m00 > m22) {
+ const S = Math.sqrt(1.0 + m00 - m11 - m22) * 2;
+ orientation[3] = (m12 - m21) / S;
+ orientation[0] = 0.25 * S;
+ orientation[1] = (m01 + m10) / S;
+ orientation[2] = (m20 + m02) / S;
+ } else if (m11 > m22) {
+ const S = Math.sqrt(1.0 + m11 - m00 - m22) * 2;
+ orientation[3] = (m20 - m02) / S;
+ orientation[0] = (m01 + m10) / S;
+ orientation[1] = 0.25 * S;
+ orientation[2] = (m12 + m21) / S;
+ } else {
+ const S = Math.sqrt(1.0 + m22 - m00 - m11) * 2;
+ orientation[3] = (m01 - m10) / S;
+ orientation[0] = (m20 + m02) / S;
+ orientation[1] = (m12 + m21) / S;
+ orientation[2] = 0.25 * S;
+ }
+
+ return { position, orientation };
+ }
+
+ static identity() {
+ return [
+ 1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 0, 0, 0, 1
+ ];
+ };
+}
+
+XRMathHelper.EPSILON = 0.001;
diff --git a/testing/web-platform/tests/resources/chromium/webxr-test-math-helper.js.headers b/testing/web-platform/tests/resources/chromium/webxr-test-math-helper.js.headers
new file mode 100644
index 0000000000..6805c323df
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/webxr-test-math-helper.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/testing/web-platform/tests/resources/chromium/webxr-test.js b/testing/web-platform/tests/resources/chromium/webxr-test.js
new file mode 100644
index 0000000000..ab2c6faa0e
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/webxr-test.js
@@ -0,0 +1,2128 @@
+import * as vrMojom from '/gen/device/vr/public/mojom/vr_service.mojom.m.js';
+import * as xrSessionMojom from '/gen/device/vr/public/mojom/xr_session.mojom.m.js';
+import {GamepadHand, GamepadMapping} from '/gen/device/gamepad/public/mojom/gamepad.mojom.m.js';
+
+// This polyfill library implements the WebXR Test API as specified here:
+// https://github.com/immersive-web/webxr-test-api
+
+const defaultMojoFromFloor = {
+ matrix: [1, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 1, 0,
+ 0, -1.65, 0, 1]
+};
+const default_stage_parameters = {
+ mojoFromFloor: defaultMojoFromFloor,
+ bounds: null
+};
+
+const default_framebuffer_scale = 0.7;
+
+function getMatrixFromTransform(transform) {
+ const x = transform.orientation[0];
+ const y = transform.orientation[1];
+ const z = transform.orientation[2];
+ const w = transform.orientation[3];
+
+ const m11 = 1.0 - 2.0 * (y * y + z * z);
+ const m21 = 2.0 * (x * y + z * w);
+ const m31 = 2.0 * (x * z - y * w);
+
+ const m12 = 2.0 * (x * y - z * w);
+ const m22 = 1.0 - 2.0 * (x * x + z * z);
+ const m32 = 2.0 * (y * z + x * w);
+
+ const m13 = 2.0 * (x * z + y * w);
+ const m23 = 2.0 * (y * z - x * w);
+ const m33 = 1.0 - 2.0 * (x * x + y * y);
+
+ const m14 = transform.position[0];
+ const m24 = transform.position[1];
+ const m34 = transform.position[2];
+
+ // Column-major linearized order is expected.
+ return [m11, m21, m31, 0,
+ m12, m22, m32, 0,
+ m13, m23, m33, 0,
+ m14, m24, m34, 1];
+}
+
+function getPoseFromTransform(transform) {
+ const [px, py, pz] = transform.position;
+ const [ox, oy, oz, ow] = transform.orientation;
+ return {
+ position: {x: px, y: py, z: pz},
+ orientation: {x: ox, y: oy, z: oz, w: ow},
+ };
+}
+
+function composeGFXTransform(fakeTransformInit) {
+ return {matrix: getMatrixFromTransform(fakeTransformInit)};
+}
+
+// Value equality for camera image init objects - they must contain `width` &
+// `height` properties and may contain `pixels` property.
+function isSameCameraImageInit(rhs, lhs) {
+ return lhs.width === rhs.width && lhs.height === rhs.height && lhs.pixels === rhs.pixels;
+}
+
+class ChromeXRTest {
+ constructor() {
+ this.mockVRService_ = new MockVRService();
+ }
+
+ // WebXR Test API
+ simulateDeviceConnection(init_params) {
+ return Promise.resolve(this.mockVRService_._addRuntime(init_params));
+ }
+
+ disconnectAllDevices() {
+ this.mockVRService_._removeAllRuntimes();
+ return Promise.resolve();
+ }
+
+ simulateUserActivation(callback) {
+ if (window.top !== window) {
+ // test_driver.click only works for the toplevel frame. This alternate
+ // Chrome-specific method is sufficient for starting an XR session in an
+ // iframe, and is used in platform-specific tests.
+ //
+ // TODO(https://github.com/web-platform-tests/wpt/issues/20282): use
+ // a cross-platform method if available.
+ xr_debug('simulateUserActivation', 'use eventSender');
+ document.addEventListener('click', callback);
+ eventSender.mouseMoveTo(0, 0);
+ eventSender.mouseDown();
+ eventSender.mouseUp();
+ document.removeEventListener('click', callback);
+ return;
+ }
+ const button = document.createElement('button');
+ button.textContent = 'click to continue test';
+ button.style.display = 'block';
+ button.style.fontSize = '20px';
+ button.style.padding = '10px';
+ button.onclick = () => {
+ callback();
+ document.body.removeChild(button);
+ };
+ document.body.appendChild(button);
+ test_driver.click(button);
+ }
+
+ // Helper method leveraged by chrome-specific setups.
+ Debug(name, msg) {
+ console.log(new Date().toISOString() + ' DEBUG[' + name + '] ' + msg);
+ }
+}
+
+// Mocking class definitions
+
+// Mock service implements the VRService mojo interface.
+class MockVRService {
+ constructor() {
+ this.receiver_ = new vrMojom.VRServiceReceiver(this);
+ this.runtimes_ = [];
+
+ this.interceptor_ =
+ new MojoInterfaceInterceptor(vrMojom.VRService.$interfaceName);
+ this.interceptor_.oninterfacerequest =
+ e => this.receiver_.$.bindHandle(e.handle);
+ this.interceptor_.start();
+ }
+
+ // WebXR Test API Implementation Helpers
+ _addRuntime(fakeDeviceInit) {
+ const runtime = new MockRuntime(fakeDeviceInit, this);
+ this.runtimes_.push(runtime);
+
+ if (this.client_) {
+ this.client_.onDeviceChanged();
+ }
+
+ return runtime;
+ }
+
+ _removeAllRuntimes() {
+ if (this.client_) {
+ this.client_.onDeviceChanged();
+ }
+
+ this.runtimes_ = [];
+ }
+
+ _removeRuntime(device) {
+ const index = this.runtimes_.indexOf(device);
+ if (index >= 0) {
+ this.runtimes_.splice(index, 1);
+ if (this.client_) {
+ this.client_.onDeviceChanged();
+ }
+ }
+ }
+
+ // VRService overrides
+ setClient(client) {
+ if (this.client_) {
+ throw new Error("setClient should only be called once");
+ }
+
+ this.client_ = client;
+ }
+
+ requestSession(sessionOptions) {
+ const requests = [];
+ // Request a session from all the runtimes.
+ for (let i = 0; i < this.runtimes_.length; i++) {
+ requests[i] = this.runtimes_[i]._requestRuntimeSession(sessionOptions);
+ }
+
+ return Promise.all(requests).then((results) => {
+ // Find and return the first successful result.
+ for (let i = 0; i < results.length; i++) {
+ if (results[i].session) {
+ // Construct a dummy metrics recorder
+ const metricsRecorderPtr = new vrMojom.XRSessionMetricsRecorderRemote();
+ metricsRecorderPtr.$.bindNewPipeAndPassReceiver().handle.close();
+
+ const success = {
+ session: results[i].session,
+ metricsRecorder: metricsRecorderPtr,
+ };
+
+ return {result: {success}};
+ }
+ }
+
+ // If there were no successful results, returns a null session.
+ return {
+ result: {failureReason: vrMojom.RequestSessionError.NO_RUNTIME_FOUND}
+ };
+ });
+ }
+
+ supportsSession(sessionOptions) {
+ const requests = [];
+ // Check supports on all the runtimes.
+ for (let i = 0; i < this.runtimes_.length; i++) {
+ requests[i] = this.runtimes_[i]._runtimeSupportsSession(sessionOptions);
+ }
+
+ return Promise.all(requests).then((results) => {
+ // Find and return the first successful result.
+ for (let i = 0; i < results.length; i++) {
+ if (results[i].supportsSession) {
+ return results[i];
+ }
+ }
+
+ // If there were no successful results, returns false.
+ return {supportsSession: false};
+ });
+ }
+
+ exitPresent() {
+ return Promise.resolve();
+ }
+
+ setFramesThrottled(throttled) {
+ this.setFramesThrottledImpl(throttled);
+ }
+
+ // We cannot override the mojom interceptors via the prototype; so this method
+ // and the above indirection exist to allow overrides by internal code.
+ setFramesThrottledImpl(throttled) {}
+
+ // Only handles asynchronous calls to makeXrCompatible. Synchronous calls are
+ // not supported in Javascript.
+ makeXrCompatible() {
+ if (this.runtimes_.length == 0) {
+ return {
+ xrCompatibleResult: vrMojom.XrCompatibleResult.kNoDeviceAvailable
+ };
+ }
+ return {xrCompatibleResult: vrMojom.XrCompatibleResult.kAlreadyCompatible};
+ }
+}
+
+class FakeXRAnchorController {
+ constructor() {
+ // Private properties.
+ this.device_ = null;
+ this.id_ = null;
+ this.dirty_ = true;
+
+ // Properties backing up public attributes / methods.
+ this.deleted_ = false;
+ this.paused_ = false;
+ this.anchorOrigin_ = XRMathHelper.identity();
+ }
+
+ // WebXR Test API (Anchors Extension)
+ get deleted() {
+ return this.deleted_;
+ }
+
+ pauseTracking() {
+ if(!this.paused_) {
+ this.paused_ = true;
+ this.dirty_ = true;
+ }
+ }
+
+ resumeTracking() {
+ if(this.paused_) {
+ this.paused_ = false;
+ this.dirty_ = true;
+ }
+ }
+
+ stopTracking() {
+ if(!this.deleted_) {
+ this.device_._deleteAnchorController(this.id_);
+
+ this.deleted_ = true;
+ this.dirty_ = true;
+ }
+ }
+
+ setAnchorOrigin(anchorOrigin) {
+ this.anchorOrigin_ = getMatrixFromTransform(anchorOrigin);
+ this.dirty_ = true;
+ }
+
+ // Internal implementation:
+ set id(value) {
+ this.id_ = value;
+ }
+
+ set device(value) {
+ this.device_ = value;
+ }
+
+ get dirty() {
+ return this.dirty_;
+ }
+
+ get paused() {
+ return this.paused_;
+ }
+
+ _markProcessed() {
+ this.dirty_ = false;
+ }
+
+ _getAnchorOrigin() {
+ return this.anchorOrigin_;
+ }
+}
+
+// Implements XRFrameDataProvider and XRPresentationProvider. Maintains a mock
+// for XRPresentationProvider. Implements FakeXRDevice test API.
+class MockRuntime {
+ // Mapping from string feature names to the corresponding mojo types.
+ // This is exposed as a member for extensibility.
+ static _featureToMojoMap = {
+ 'viewer': xrSessionMojom.XRSessionFeature.REF_SPACE_VIEWER,
+ 'local': xrSessionMojom.XRSessionFeature.REF_SPACE_LOCAL,
+ 'local-floor': xrSessionMojom.XRSessionFeature.REF_SPACE_LOCAL_FLOOR,
+ 'bounded-floor': xrSessionMojom.XRSessionFeature.REF_SPACE_BOUNDED_FLOOR,
+ 'unbounded': xrSessionMojom.XRSessionFeature.REF_SPACE_UNBOUNDED,
+ 'hit-test': xrSessionMojom.XRSessionFeature.HIT_TEST,
+ 'dom-overlay': xrSessionMojom.XRSessionFeature.DOM_OVERLAY,
+ 'light-estimation': xrSessionMojom.XRSessionFeature.LIGHT_ESTIMATION,
+ 'anchors': xrSessionMojom.XRSessionFeature.ANCHORS,
+ 'depth-sensing': xrSessionMojom.XRSessionFeature.DEPTH,
+ 'secondary-views': xrSessionMojom.XRSessionFeature.SECONDARY_VIEWS,
+ 'camera-access': xrSessionMojom.XRSessionFeature.CAMERA_ACCESS,
+ 'layers': xrSessionMojom.XRSessionFeature.LAYERS,
+ };
+
+ static _sessionModeToMojoMap = {
+ "inline": xrSessionMojom.XRSessionMode.kInline,
+ "immersive-vr": xrSessionMojom.XRSessionMode.kImmersiveVr,
+ "immersive-ar": xrSessionMojom.XRSessionMode.kImmersiveAr,
+ };
+
+ static _environmentBlendModeToMojoMap = {
+ "opaque": vrMojom.XREnvironmentBlendMode.kOpaque,
+ "alpha-blend": vrMojom.XREnvironmentBlendMode.kAlphaBlend,
+ "additive": vrMojom.XREnvironmentBlendMode.kAdditive,
+ };
+
+ static _interactionModeToMojoMap = {
+ "screen-space": vrMojom.XRInteractionMode.kScreenSpace,
+ "world-space": vrMojom.XRInteractionMode.kWorldSpace,
+ };
+
+ constructor(fakeDeviceInit, service) {
+ this.sessionClient_ = null;
+ this.presentation_provider_ = new MockXRPresentationProvider();
+
+ this.pose_ = null;
+ this.next_frame_id_ = 0;
+ this.bounds_ = null;
+ this.send_mojo_space_reset_ = false;
+ this.stageParameters_ = null;
+ this.stageParametersId_ = 1;
+
+ this.service_ = service;
+
+ this.framesOfReference = {};
+
+ this.input_sources_ = new Map();
+ this.next_input_source_index_ = 1;
+
+ // Currently active hit test subscriptons.
+ this.hitTestSubscriptions_ = new Map();
+ // Currently active transient hit test subscriptions.
+ this.transientHitTestSubscriptions_ = new Map();
+ // ID of the next subscription to be assigned.
+ this.next_hit_test_id_ = 1n;
+
+ this.anchor_controllers_ = new Map();
+ // ID of the next anchor to be assigned.
+ this.next_anchor_id_ = 1n;
+ // Anchor creation callback (initially null, can be set by tests).
+ this.anchor_creation_callback_ = null;
+
+ this.depthSensingData_ = null;
+ this.depthSensingDataDirty_ = false;
+
+ let supportedModes = [];
+ if (fakeDeviceInit.supportedModes) {
+ supportedModes = fakeDeviceInit.supportedModes.slice();
+ if (fakeDeviceInit.supportedModes.length === 0) {
+ supportedModes = ["inline"];
+ }
+ } else {
+ // Back-compat mode.
+ console.warn("Please use `supportedModes` to signal which modes are supported by this device.");
+ if (fakeDeviceInit.supportsImmersive == null) {
+ throw new TypeError("'supportsImmersive' must be set");
+ }
+
+ supportedModes = ["inline"];
+ if (fakeDeviceInit.supportsImmersive) {
+ supportedModes.push("immersive-vr");
+ }
+ }
+
+ this.supportedModes_ = this._convertModesToEnum(supportedModes);
+ if (this.supportedModes_.length == 0) {
+ console.error("Device has empty supported modes array!");
+ throw new InvalidStateError();
+ }
+
+ if (fakeDeviceInit.viewerOrigin != null) {
+ this.setViewerOrigin(fakeDeviceInit.viewerOrigin);
+ }
+
+ if (fakeDeviceInit.floorOrigin != null) {
+ this.setFloorOrigin(fakeDeviceInit.floorOrigin);
+ }
+
+ if (fakeDeviceInit.world) {
+ this.setWorld(fakeDeviceInit.world);
+ }
+
+ if (fakeDeviceInit.depthSensingData) {
+ this.setDepthSensingData(fakeDeviceInit.depthSensingData);
+ }
+
+ this.defaultFramebufferScale_ = default_framebuffer_scale;
+ this.enviromentBlendMode_ = this._convertBlendModeToEnum(fakeDeviceInit.environmentBlendMode);
+ this.interactionMode_ = this._convertInteractionModeToEnum(fakeDeviceInit.interactionMode);
+
+ // This appropriately handles if the coordinates are null
+ this.setBoundsGeometry(fakeDeviceInit.boundsCoordinates);
+
+ this.setViews(fakeDeviceInit.views, fakeDeviceInit.secondaryViews);
+
+ // Need to support webVR which doesn't have a notion of features
+ this._setFeatures(fakeDeviceInit.supportedFeatures || []);
+ }
+
+ // WebXR Test API
+ setViews(primaryViews, secondaryViews) {
+ this.cameraImage_ = null;
+ this.primaryViews_ = [];
+ this.secondaryViews_ = [];
+ let xOffset = 0;
+ if (primaryViews) {
+ this.primaryViews_ = [];
+ xOffset = this._setViews(primaryViews, xOffset, this.primaryViews_);
+ const cameraImage = this._findCameraImage(primaryViews);
+
+ if (cameraImage) {
+ this.cameraImage_ = cameraImage;
+ }
+ }
+
+ if (secondaryViews) {
+ this.secondaryViews_ = [];
+ this._setViews(secondaryViews, xOffset, this.secondaryViews_);
+ const cameraImage = this._findCameraImage(secondaryViews);
+
+ if (cameraImage) {
+ if (!isSameCameraImageInit(this.cameraImage_, cameraImage)) {
+ throw new Error("If present, camera resolutions on each view must match each other!"
+ + " Secondary views' camera doesn't match primary views.");
+ }
+
+ this.cameraImage_ = cameraImage;
+ }
+ }
+ }
+
+ disconnect() {
+ this.service_._removeRuntime(this);
+ this.presentation_provider_._close();
+ if (this.sessionClient_) {
+ this.sessionClient_.$.close();
+ this.sessionClient_ = null;
+ }
+
+ return Promise.resolve();
+ }
+
+ setViewerOrigin(origin, emulatedPosition = false) {
+ const p = origin.position;
+ const q = origin.orientation;
+ this.pose_ = {
+ orientation: { x: q[0], y: q[1], z: q[2], w: q[3] },
+ position: { x: p[0], y: p[1], z: p[2] },
+ emulatedPosition: emulatedPosition,
+ angularVelocity: null,
+ linearVelocity: null,
+ angularAcceleration: null,
+ linearAcceleration: null,
+ inputState: null,
+ poseIndex: 0
+ };
+ }
+
+ clearViewerOrigin() {
+ this.pose_ = null;
+ }
+
+ setFloorOrigin(floorOrigin) {
+ if (!this.stageParameters_) {
+ this.stageParameters_ = default_stage_parameters;
+ this.stageParameters_.bounds = this.bounds_;
+ }
+
+ // floorOrigin is passed in as mojoFromFloor.
+ this.stageParameters_.mojoFromFloor =
+ {matrix: getMatrixFromTransform(floorOrigin)};
+
+ this._onStageParametersUpdated();
+ }
+
+ clearFloorOrigin() {
+ if (this.stageParameters_) {
+ this.stageParameters_ = null;
+ this._onStageParametersUpdated();
+ }
+ }
+
+ setBoundsGeometry(bounds) {
+ if (bounds == null) {
+ this.bounds_ = null;
+ } else if (bounds.length < 3) {
+ throw new Error("Bounds must have a length of at least 3");
+ } else {
+ this.bounds_ = bounds;
+ }
+
+ // We can only set bounds if we have stageParameters set; otherwise, we
+ // don't know the transform from local space to bounds space.
+ // We'll cache the bounds so that they can be set in the future if the
+ // floorLevel transform is set, but we won't update them just yet.
+ if (this.stageParameters_) {
+ this.stageParameters_.bounds = this.bounds_;
+ this._onStageParametersUpdated();
+ }
+ }
+
+ simulateResetPose() {
+ this.send_mojo_space_reset_ = true;
+ }
+
+ simulateVisibilityChange(visibilityState) {
+ let mojoState = null;
+ switch (visibilityState) {
+ case "visible":
+ mojoState = vrMojom.XRVisibilityState.VISIBLE;
+ break;
+ case "visible-blurred":
+ mojoState = vrMojom.XRVisibilityState.VISIBLE_BLURRED;
+ break;
+ case "hidden":
+ mojoState = vrMojom.XRVisibilityState.HIDDEN;
+ break;
+ }
+ if (mojoState && this.sessionClient_) {
+ this.sessionClient_.onVisibilityStateChanged(mojoState);
+ }
+ }
+
+ simulateInputSourceConnection(fakeInputSourceInit) {
+ const index = this.next_input_source_index_;
+ this.next_input_source_index_++;
+
+ const source = new MockXRInputSource(fakeInputSourceInit, index, this);
+ this.input_sources_.set(index, source);
+ return source;
+ }
+
+ // WebXR Test API Hit Test extensions
+ setWorld(world) {
+ this.world_ = world;
+ }
+
+ clearWorld() {
+ this.world_ = null;
+ }
+
+ // WebXR Test API Anchor extensions
+ setAnchorCreationCallback(callback) {
+ this.anchor_creation_callback_ = callback;
+ }
+
+ setHitTestSourceCreationCallback(callback) {
+ this.hit_test_source_creation_callback_ = callback;
+ }
+
+ // WebXR Test API Lighting estimation extensions
+ setLightEstimate(fakeXrLightEstimateInit) {
+ if (!fakeXrLightEstimateInit.sphericalHarmonicsCoefficients) {
+ throw new TypeError("sphericalHarmonicsCoefficients must be set");
+ }
+
+ if (fakeXrLightEstimateInit.sphericalHarmonicsCoefficients.length != 27) {
+ throw new TypeError("Must supply all 27 sphericalHarmonicsCoefficients");
+ }
+
+ if (fakeXrLightEstimateInit.primaryLightDirection && fakeXrLightEstimateInit.primaryLightDirection.w != 0) {
+ throw new TypeError("W component of primaryLightDirection must be 0");
+ }
+
+ if (fakeXrLightEstimateInit.primaryLightIntensity && fakeXrLightEstimateInit.primaryLightIntensity.w != 1) {
+ throw new TypeError("W component of primaryLightIntensity must be 1");
+ }
+
+ // If the primaryLightDirection or primaryLightIntensity aren't set, we need to set them
+ // to the defaults that the spec expects. ArCore will either give us everything or nothing,
+ // so these aren't nullable on the mojom.
+ if (!fakeXrLightEstimateInit.primaryLightDirection) {
+ fakeXrLightEstimateInit.primaryLightDirection = { x: 0.0, y: 1.0, z: 0.0, w: 0.0 };
+ }
+
+ if (!fakeXrLightEstimateInit.primaryLightIntensity) {
+ fakeXrLightEstimateInit.primaryLightIntensity = { x: 0.0, y: 0.0, z: 0.0, w: 1.0 };
+ }
+
+ let c = fakeXrLightEstimateInit.sphericalHarmonicsCoefficients;
+
+ this.light_estimate_ = {
+ lightProbe: {
+ // XRSphereicalHarmonics
+ sphericalHarmonics: {
+ coefficients: [
+ { red: c[0], green: c[1], blue: c[2] },
+ { red: c[3], green: c[4], blue: c[5] },
+ { red: c[6], green: c[7], blue: c[8] },
+ { red: c[9], green: c[10], blue: c[11] },
+ { red: c[12], green: c[13], blue: c[14] },
+ { red: c[15], green: c[16], blue: c[17] },
+ { red: c[18], green: c[19], blue: c[20] },
+ { red: c[21], green: c[22], blue: c[23] },
+ { red: c[24], green: c[25], blue: c[26] }
+ ]
+ },
+ // Vector3dF
+ mainLightDirection: {
+ x: fakeXrLightEstimateInit.primaryLightDirection.x,
+ y: fakeXrLightEstimateInit.primaryLightDirection.y,
+ z: fakeXrLightEstimateInit.primaryLightDirection.z
+ },
+ // RgbTupleF32
+ mainLightIntensity: {
+ red: fakeXrLightEstimateInit.primaryLightIntensity.x,
+ green: fakeXrLightEstimateInit.primaryLightIntensity.y,
+ blue: fakeXrLightEstimateInit.primaryLightIntensity.z
+ }
+ }
+ }
+ }
+
+ // WebXR Test API depth Sensing Extensions
+ setDepthSensingData(depthSensingData) {
+ for(const key of ["depthData", "normDepthBufferFromNormView", "rawValueToMeters", "width", "height"]) {
+ if(!(key in depthSensingData)) {
+ throw new TypeError("Required key not present. Key: " + key);
+ }
+ }
+
+ if(depthSensingData.depthData != null) {
+ // Create new object w/ properties based on the depthSensingData, but
+ // convert the FakeXRRigidTransformInit into a transformation matrix object.
+ this.depthSensingData_ = Object.assign({},
+ depthSensingData, {
+ normDepthBufferFromNormView: composeGFXTransform(depthSensingData.normDepthBufferFromNormView),
+ });
+ } else {
+ throw new TypeError("`depthData` is not set");
+ }
+
+ this.depthSensingDataDirty_ = true;
+ }
+
+ clearDepthSensingData() {
+ this.depthSensingData_ = null;
+ this.depthSensingDataDirty_ = true;
+ }
+
+ // Internal Implementation/Helper Methods
+ _convertModeToEnum(sessionMode) {
+ if (sessionMode in MockRuntime._sessionModeToMojoMap) {
+ return MockRuntime._sessionModeToMojoMap[sessionMode];
+ }
+
+ throw new TypeError("Unrecognized value for XRSessionMode enum: " + sessionMode);
+ }
+
+ _convertModesToEnum(sessionModes) {
+ return sessionModes.map(mode => this._convertModeToEnum(mode));
+ }
+
+ _convertBlendModeToEnum(blendMode) {
+ if (blendMode in MockRuntime._environmentBlendModeToMojoMap) {
+ return MockRuntime._environmentBlendModeToMojoMap[blendMode];
+ } else {
+ if (this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
+ return vrMojom.XREnvironmentBlendMode.kAdditive;
+ } else if (this.supportedModes_.includes(
+ xrSessionMojom.XRSessionMode.kImmersiveVr)) {
+ return vrMojom.XREnvironmentBlendMode.kOpaque;
+ }
+ }
+ }
+
+ _convertInteractionModeToEnum(interactionMode) {
+ if (interactionMode in MockRuntime._interactionModeToMojoMap) {
+ return MockRuntime._interactionModeToMojoMap[interactionMode];
+ } else {
+ return vrMojom.XRInteractionMode.kWorldSpace;
+ }
+ }
+
+ _setViews(deviceViews, xOffset, views) {
+ for (let i = 0; i < deviceViews.length; i++) {
+ views[i] = this._getView(deviceViews[i], xOffset);
+ xOffset += deviceViews[i].resolution.width;
+ }
+
+ return xOffset;
+ }
+
+ _findCameraImage(views) {
+ const viewWithCamera = views.find(view => view.cameraImageInit);
+ if (viewWithCamera) {
+ //If we have one view with a camera resolution, all views should have the same camera resolution.
+ const allViewsHaveSameCamera = views.every(
+ view => isSameCameraImageInit(view.cameraImageInit, viewWithCamera.cameraImageInit));
+
+ if (!allViewsHaveSameCamera) {
+ throw new Error("If present, camera resolutions on each view must match each other!");
+ }
+
+ return viewWithCamera.cameraImageInit;
+ }
+
+ return null;
+ }
+
+ _onStageParametersUpdated() {
+ // Indicate for the frame loop that the stage parameters have been updated.
+ this.stageParametersId_++;
+ }
+
+ _getDefaultViews() {
+ if (this.primaryViews_) {
+ return this.primaryViews_;
+ }
+
+ const viewport_size = 20;
+ return [{
+ eye: vrMojom.XREye.kLeft,
+ fieldOfView: {
+ upDegrees: 48.316,
+ downDegrees: 50.099,
+ leftDegrees: 50.899,
+ rightDegrees: 35.197
+ },
+ mojoFromView: this._getMojoFromViewerWithOffset(composeGFXTransform({
+ position: [-0.032, 0, 0],
+ orientation: [0, 0, 0, 1]
+ })),
+ viewport: { x: 0, y: 0, width: viewport_size, height: viewport_size }
+ },
+ {
+ eye: vrMojom.XREye.kRight,
+ fieldOfView: {
+ upDegrees: 48.316,
+ downDegrees: 50.099,
+ leftDegrees: 50.899,
+ rightDegrees: 35.197
+ },
+ mojoFromView: this._getMojoFromViewerWithOffset(composeGFXTransform({
+ position: [0.032, 0, 0],
+ orientation: [0, 0, 0, 1]
+ })),
+ viewport: { x: viewport_size, y: 0, width: viewport_size, height: viewport_size }
+ }];
+ }
+
+ // This function converts between the matrix provided by the WebXR test API
+ // and the internal data representation.
+ _getView(fakeXRViewInit, xOffset) {
+ let fov = null;
+
+ if (fakeXRViewInit.fieldOfView) {
+ fov = {
+ upDegrees: fakeXRViewInit.fieldOfView.upDegrees,
+ downDegrees: fakeXRViewInit.fieldOfView.downDegrees,
+ leftDegrees: fakeXRViewInit.fieldOfView.leftDegrees,
+ rightDegrees: fakeXRViewInit.fieldOfView.rightDegrees
+ };
+ } else {
+ const m = fakeXRViewInit.projectionMatrix;
+
+ function toDegrees(tan) {
+ return Math.atan(tan) * 180 / Math.PI;
+ }
+
+ const leftTan = (1 - m[8]) / m[0];
+ const rightTan = (1 + m[8]) / m[0];
+ const upTan = (1 + m[9]) / m[5];
+ const downTan = (1 - m[9]) / m[5];
+
+ fov = {
+ upDegrees: toDegrees(upTan),
+ downDegrees: toDegrees(downTan),
+ leftDegrees: toDegrees(leftTan),
+ rightDegrees: toDegrees(rightTan)
+ };
+ }
+
+ let viewEye = vrMojom.XREye.kNone;
+ // The eye passed in corresponds to the values in the WebXR spec, which are
+ // the strings "none", "left", and "right". They should be converted to the
+ // corresponding values of XREye in vr_service.mojom.
+ switch(fakeXRViewInit.eye) {
+ case "none":
+ viewEye = vrMojom.XREye.kNone;
+ break;
+ case "left":
+ viewEye = vrMojom.XREye.kLeft;
+ break;
+ case "right":
+ viewEye = vrMojom.XREye.kRight;
+ break;
+ }
+
+ return {
+ eye: viewEye,
+ fieldOfView: fov,
+ mojoFromView: this._getMojoFromViewerWithOffset(composeGFXTransform(fakeXRViewInit.viewOffset)),
+ viewport: {
+ x: xOffset,
+ y: 0,
+ width: fakeXRViewInit.resolution.width,
+ height: fakeXRViewInit.resolution.height
+ },
+ isFirstPersonObserver: fakeXRViewInit.isFirstPersonObserver ? true : false,
+ viewOffset: composeGFXTransform(fakeXRViewInit.viewOffset)
+ };
+ }
+
+ _setFeatures(supportedFeatures) {
+ function convertFeatureToMojom(feature) {
+ if (feature in MockRuntime._featureToMojoMap) {
+ return MockRuntime._featureToMojoMap[feature];
+ } else {
+ return xrSessionMojom.XRSessionFeature.INVALID;
+ }
+ }
+
+ this.supportedFeatures_ = [];
+
+ for (let i = 0; i < supportedFeatures.length; i++) {
+ const feature = convertFeatureToMojom(supportedFeatures[i]);
+ if (feature !== xrSessionMojom.XRSessionFeature.INVALID) {
+ this.supportedFeatures_.push(feature);
+ }
+ }
+ }
+
+ // These methods are intended to be used by MockXRInputSource only.
+ _addInputSource(source) {
+ if (!this.input_sources_.has(source.source_id_)) {
+ this.input_sources_.set(source.source_id_, source);
+ }
+ }
+
+ _removeInputSource(source) {
+ this.input_sources_.delete(source.source_id_);
+ }
+
+ // These methods are intended to be used by FakeXRAnchorController only.
+ _deleteAnchorController(controllerId) {
+ this.anchor_controllers_.delete(controllerId);
+ }
+
+ // Extension point for non-standard modules.
+ _injectAdditionalFrameData(options, frameData) {
+ }
+
+ // Mojo function implementations.
+
+ // XRFrameDataProvider implementation.
+ getFrameData(options) {
+ return new Promise((resolve) => {
+
+ const populatePose = () => {
+ const mojo_space_reset = this.send_mojo_space_reset_;
+ this.send_mojo_space_reset_ = false;
+
+ if (this.pose_) {
+ this.pose_.poseIndex++;
+ }
+
+ // Setting the input_state to null tests a slightly different path than
+ // the browser tests where if the last input source is removed, the device
+ // code always sends up an empty array, but it's also valid mojom to send
+ // up a null array.
+ let input_state = null;
+ if (this.input_sources_.size > 0) {
+ input_state = [];
+ for (const input_source of this.input_sources_.values()) {
+ input_state.push(input_source._getInputSourceState());
+ }
+ }
+
+ let frame_views = this.primaryViews_;
+ for (let i = 0; i < this.primaryViews_.length; i++) {
+ this.primaryViews_[i].mojoFromView =
+ this._getMojoFromViewerWithOffset(this.primaryViews_[i].viewOffset);
+ }
+ if (this.enabledFeatures_.includes(xrSessionMojom.XRSessionFeature.SECONDARY_VIEWS)) {
+ for (let i = 0; i < this.secondaryViews_.length; i++) {
+ this.secondaryViews_[i].mojoFromView =
+ this._getMojoFromViewerWithOffset(this.secondaryViews_[i].viewOffset);
+ }
+
+ frame_views = frame_views.concat(this.secondaryViews_);
+ }
+
+ const frameData = {
+ mojoFromViewer: this.pose_,
+ views: frame_views,
+ mojoSpaceReset: mojo_space_reset,
+ inputState: input_state,
+ timeDelta: {
+ // window.performance.now() is in milliseconds, so convert to microseconds.
+ microseconds: BigInt(Math.floor(window.performance.now() * 1000)),
+ },
+ frameId: this.next_frame_id_,
+ bufferHolder: null,
+ cameraImageSize: this.cameraImage_ ? {
+ width: this.cameraImage_.width,
+ height: this.cameraImage_.height
+ } : null,
+ renderingTimeRatio: 0,
+ stageParameters: this.stageParameters_,
+ stageParametersId: this.stageParametersId_,
+ lightEstimationData: this.light_estimate_
+ };
+
+ this.next_frame_id_++;
+
+ this._calculateHitTestResults(frameData);
+
+ this._calculateAnchorInformation(frameData);
+
+ this._calculateDepthInformation(frameData);
+
+ this._injectAdditionalFrameData(options, frameData);
+
+ resolve({frameData});
+ };
+
+ if(this.sessionOptions_.mode == xrSessionMojom.XRSessionMode.kInline) {
+ // Inline sessions should not have a delay introduced since it causes them
+ // to miss a vsync blink-side and delays propagation of changes that happened
+ // within a rAFcb by one frame (e.g. setViewerOrigin() calls would take 2 frames
+ // to propagate).
+ populatePose();
+ } else {
+ // For immerive sessions, add additional delay to allow for anchor creation
+ // promises to run.
+ setTimeout(populatePose, 3); // note: according to MDN, the timeout is not exact
+ }
+ });
+ }
+
+ getEnvironmentIntegrationProvider(environmentProviderRequest) {
+ if (this.environmentProviderReceiver_) {
+ this.environmentProviderReceiver_.$.close();
+ }
+ this.environmentProviderReceiver_ =
+ new vrMojom.XREnvironmentIntegrationProviderReceiver(this);
+ this.environmentProviderReceiver_.$.bindHandle(
+ environmentProviderRequest.handle);
+ }
+
+ setInputSourceButtonListener(listener) { listener.$.close(); }
+
+ // XREnvironmentIntegrationProvider implementation:
+ subscribeToHitTest(nativeOriginInformation, entityTypes, ray) {
+ if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
+ // Reject outside of AR.
+ return Promise.resolve({
+ result : vrMojom.SubscribeToHitTestResult.FAILURE_GENERIC,
+ subscriptionId : 0n
+ });
+ }
+
+ if (!this._nativeOriginKnown(nativeOriginInformation)) {
+ return Promise.resolve({
+ result : vrMojom.SubscribeToHitTestResult.FAILURE_GENERIC,
+ subscriptionId : 0n
+ });
+ }
+
+ // Reserve the id for hit test source:
+ const id = this.next_hit_test_id_++;
+ const hitTestParameters = { isTransient: false, profileName: null };
+ const controller = new FakeXRHitTestSourceController(id);
+
+
+ return this._shouldHitTestSourceCreationSucceed(hitTestParameters, controller)
+ .then((succeeded) => {
+ if(succeeded) {
+ // Store the subscription information as-is (including controller):
+ this.hitTestSubscriptions_.set(id, { nativeOriginInformation, entityTypes, ray, controller });
+
+ return Promise.resolve({
+ result : vrMojom.SubscribeToHitTestResult.SUCCESS,
+ subscriptionId : id
+ });
+ } else {
+ return Promise.resolve({
+ result : vrMojom.SubscribeToHitTestResult.FAILURE_GENERIC,
+ subscriptionId : 0n
+ });
+ }
+ });
+ }
+
+ subscribeToHitTestForTransientInput(profileName, entityTypes, ray){
+ if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
+ // Reject outside of AR.
+ return Promise.resolve({
+ result : vrMojom.SubscribeToHitTestResult.FAILURE_GENERIC,
+ subscriptionId : 0n
+ });
+ }
+
+ const id = this.next_hit_test_id_++;
+ const hitTestParameters = { isTransient: true, profileName: profileName };
+ const controller = new FakeXRHitTestSourceController(id);
+
+ // Check if we have hit test source creation callback.
+ // If yes, ask it if the hit test source creation should succeed.
+ // If no, for back-compat, assume the hit test source creation succeeded.
+ return this._shouldHitTestSourceCreationSucceed(hitTestParameters, controller)
+ .then((succeeded) => {
+ if(succeeded) {
+ // Store the subscription information as-is (including controller):
+ this.transientHitTestSubscriptions_.set(id, { profileName, entityTypes, ray, controller });
+
+ return Promise.resolve({
+ result : vrMojom.SubscribeToHitTestResult.SUCCESS,
+ subscriptionId : id
+ });
+ } else {
+ return Promise.resolve({
+ result : vrMojom.SubscribeToHitTestResult.FAILURE_GENERIC,
+ subscriptionId : 0n
+ });
+ }
+ });
+ }
+
+ unsubscribeFromHitTest(subscriptionId) {
+ let controller = null;
+ if(this.transientHitTestSubscriptions_.has(subscriptionId)){
+ controller = this.transientHitTestSubscriptions_.get(subscriptionId).controller;
+ this.transientHitTestSubscriptions_.delete(subscriptionId);
+ } else if(this.hitTestSubscriptions_.has(subscriptionId)){
+ controller = this.hitTestSubscriptions_.get(subscriptionId).controller;
+ this.hitTestSubscriptions_.delete(subscriptionId);
+ }
+
+ if(controller) {
+ controller.deleted = true;
+ }
+ }
+
+ createAnchor(nativeOriginInformation, nativeOriginFromAnchor) {
+ return new Promise((resolve) => {
+ if(this.anchor_creation_callback_ == null) {
+ resolve({
+ result : vrMojom.CreateAnchorResult.FAILURE,
+ anchorId : 0n
+ });
+
+ return;
+ }
+
+ const mojoFromNativeOrigin = this._getMojoFromNativeOrigin(nativeOriginInformation);
+ if(mojoFromNativeOrigin == null) {
+ resolve({
+ result : vrMojom.CreateAnchorResult.FAILURE,
+ anchorId : 0n
+ });
+
+ return;
+ }
+
+ const mojoFromAnchor = XRMathHelper.mul4x4(mojoFromNativeOrigin, nativeOriginFromAnchor);
+
+ const anchorCreationParameters = {
+ requestedAnchorOrigin: mojoFromAnchor,
+ isAttachedToEntity: false,
+ };
+
+ const anchorController = new FakeXRAnchorController();
+
+ this.anchor_creation_callback_(anchorCreationParameters, anchorController)
+ .then((result) => {
+ if(result) {
+ // If the test allowed the anchor creation,
+ // store the anchor controller & return success.
+
+ const anchor_id = this.next_anchor_id_;
+ this.next_anchor_id_++;
+
+ this.anchor_controllers_.set(anchor_id, anchorController);
+ anchorController.device = this;
+ anchorController.id = anchor_id;
+
+ resolve({
+ result : vrMojom.CreateAnchorResult.SUCCESS,
+ anchorId : anchor_id
+ });
+ } else {
+ // The test has rejected anchor creation.
+ resolve({
+ result : vrMojom.CreateAnchorResult.FAILURE,
+ anchorId : 0n
+ });
+ }
+ })
+ .catch(() => {
+ // The test threw an error, treat anchor creation as failed.
+ resolve({
+ result : vrMojom.CreateAnchorResult.FAILURE,
+ anchorId : 0n
+ });
+ });
+ });
+ }
+
+ createPlaneAnchor(planeFromAnchor, planeId) {
+ return new Promise((resolve) => {
+
+ // Not supported yet.
+
+ resolve({
+ result : vrMojom.CreateAnchorResult.FAILURE,
+ anchorId : 0n,
+ });
+ });
+ }
+
+ detachAnchor(anchorId) {}
+
+ // Utility function
+ _requestRuntimeSession(sessionOptions) {
+ return this._runtimeSupportsSession(sessionOptions).then((result) => {
+ // The JavaScript bindings convert c_style_names to camelCase names.
+ const options = {
+ transportMethod:
+ vrMojom.XRPresentationTransportMethod.SUBMIT_AS_MAILBOX_HOLDER,
+ waitForTransferNotification: true,
+ waitForRenderNotification: true,
+ waitForGpuFence: false,
+ };
+
+ let submit_frame_sink;
+ if (result.supportsSession) {
+ submit_frame_sink = {
+ clientReceiver: this.presentation_provider_._getClientReceiver(),
+ provider: this.presentation_provider_._bindProvider(sessionOptions),
+ transportOptions: options
+ };
+
+ const dataProviderPtr = new vrMojom.XRFrameDataProviderRemote();
+ this.dataProviderReceiver_ =
+ new vrMojom.XRFrameDataProviderReceiver(this);
+ this.dataProviderReceiver_.$.bindHandle(
+ dataProviderPtr.$.bindNewPipeAndPassReceiver().handle);
+ this.sessionOptions_ = sessionOptions;
+
+ this.sessionClient_ = new vrMojom.XRSessionClientRemote();
+ const clientReceiver = this.sessionClient_.$.bindNewPipeAndPassReceiver();
+
+ const enabled_features = [];
+ for (let i = 0; i < sessionOptions.requiredFeatures.length; i++) {
+ if (this.supportedFeatures_.indexOf(sessionOptions.requiredFeatures[i]) !== -1) {
+ enabled_features.push(sessionOptions.requiredFeatures[i]);
+ } else {
+ return Promise.resolve({session: null});
+ }
+ }
+
+ for (let i =0; i < sessionOptions.optionalFeatures.length; i++) {
+ if (this.supportedFeatures_.indexOf(sessionOptions.optionalFeatures[i]) !== -1) {
+ enabled_features.push(sessionOptions.optionalFeatures[i]);
+ }
+ }
+
+ this.enabledFeatures_ = enabled_features;
+
+ return Promise.resolve({
+ session: {
+ submitFrameSink: submit_frame_sink,
+ dataProvider: dataProviderPtr,
+ clientReceiver: clientReceiver,
+ enabledFeatures: enabled_features,
+ deviceConfig: {
+ usesInputEventing: false,
+ defaultFramebufferScale: this.defaultFramebufferScale_,
+ supportsViewportScaling: true,
+ depthConfiguration:
+ enabled_features.includes(xrSessionMojom.XRSessionFeature.DEPTH) ? {
+ depthUsage: vrMojom.XRDepthUsage.kCPUOptimized,
+ depthDataFormat: vrMojom.XRDepthDataFormat.kLuminanceAlpha,
+ } : null,
+ views: this._getDefaultViews(),
+ },
+ enviromentBlendMode: this.enviromentBlendMode_,
+ interactionMode: this.interactionMode_
+ }
+ });
+ } else {
+ return Promise.resolve({session: null});
+ }
+ });
+ }
+
+ _runtimeSupportsSession(options) {
+ let result = this.supportedModes_.includes(options.mode);
+
+ if (options.requiredFeatures.includes(xrSessionMojom.XRSessionFeature.DEPTH)
+ || options.optionalFeatures.includes(xrSessionMojom.XRSessionFeature.DEPTH)) {
+ result &= options.depthOptions.usagePreferences.includes(vrMojom.XRDepthUsage.kCPUOptimized);
+ result &= options.depthOptions.dataFormatPreferences.includes(vrMojom.XRDepthDataFormat.kLuminanceAlpha);
+ }
+
+ return Promise.resolve({
+ supportsSession: result,
+ });
+ }
+
+ // Private functions - utilities:
+ _nativeOriginKnown(nativeOriginInformation){
+
+ if (nativeOriginInformation.inputSourceSpaceInfo !== undefined) {
+ if (!this.input_sources_.has(nativeOriginInformation.inputSourceSpaceInfo.inputSourceId)) {
+ // Unknown input source.
+ return false;
+ }
+
+ return true;
+ } else if (nativeOriginInformation.referenceSpaceType !== undefined) {
+ // Bounded_floor & unbounded ref spaces are not yet supported for AR:
+ if (nativeOriginInformation.referenceSpaceType == vrMojom.XRReferenceSpaceType.kUnbounded
+ || nativeOriginInformation.referenceSpaceType == vrMojom.XRReferenceSpaceType.kBoundedFloor) {
+ return false;
+ }
+
+ return true;
+ } else {
+ // Planes and anchors are not yet supported by the mock interface.
+ return false;
+ }
+ }
+
+ // Private functions - anchors implementation:
+
+ // Modifies passed in frameData to add anchor information.
+ _calculateAnchorInformation(frameData) {
+ if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
+ return;
+ }
+
+ frameData.anchorsData = {allAnchorsIds: [], updatedAnchorsData: []};
+ for(const [id, controller] of this.anchor_controllers_) {
+ frameData.anchorsData.allAnchorsIds.push(id);
+
+ // Send the entire anchor data over if there was a change since last GetFrameData().
+ if(controller.dirty) {
+ const anchorData = {id};
+ if(!controller.paused) {
+ anchorData.mojoFromAnchor = getPoseFromTransform(
+ XRMathHelper.decomposeRigidTransform(
+ controller._getAnchorOrigin()));
+ }
+
+ controller._markProcessed();
+
+ frameData.anchorsData.updatedAnchorsData.push(anchorData);
+ }
+ }
+ }
+
+ // Private functions - depth sensing implementation:
+
+ // Modifies passed in frameData to add anchor information.
+ _calculateDepthInformation(frameData) {
+ if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
+ return;
+ }
+
+ if (!this.enabledFeatures_.includes(xrSessionMojom.XRSessionFeature.DEPTH)) {
+ return;
+ }
+
+ // If we don't have a current depth data, we'll return null
+ // (i.e. no data is not a valid data, so it cannot be "StillValid").
+ if (this.depthSensingData_ == null) {
+ frameData.depthData = null;
+ return;
+ }
+
+ if(!this.depthSensingDataDirty_) {
+ frameData.depthData = { dataStillValid: {}};
+ return;
+ }
+
+ frameData.depthData = {
+ updatedDepthData: {
+ timeDelta: frameData.timeDelta,
+ normTextureFromNormView: this.depthSensingData_.normDepthBufferFromNormView,
+ rawValueToMeters: this.depthSensingData_.rawValueToMeters,
+ size: { width: this.depthSensingData_.width, height: this.depthSensingData_.height },
+ pixelData: { bytes: this.depthSensingData_.depthData }
+ }
+ };
+
+ this.depthSensingDataDirty_ = false;
+ }
+
+ // Private functions - hit test implementation:
+
+ // Returns a Promise<bool> that signifies whether hit test source creation should succeed.
+ // If we have a hit test source creation callback installed, invoke it and return its result.
+ // If it's not installed, for back-compat just return a promise that resolves to true.
+ _shouldHitTestSourceCreationSucceed(hitTestParameters, controller) {
+ if(this.hit_test_source_creation_callback_) {
+ return this.hit_test_source_creation_callback_(hitTestParameters, controller);
+ } else {
+ return Promise.resolve(true);
+ }
+ }
+
+ // Modifies passed in frameData to add hit test results.
+ _calculateHitTestResults(frameData) {
+ if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) {
+ return;
+ }
+
+ frameData.hitTestSubscriptionResults = {results: [],
+ transientInputResults: []};
+ if (!this.world_) {
+ return;
+ }
+
+ // Non-transient hit test:
+ for (const [id, subscription] of this.hitTestSubscriptions_) {
+ const mojo_from_native_origin = this._getMojoFromNativeOrigin(subscription.nativeOriginInformation);
+ if (!mojo_from_native_origin) continue;
+
+ const [mojo_ray_origin, mojo_ray_direction] = this._transformRayToMojoSpace(
+ subscription.ray,
+ mojo_from_native_origin
+ );
+
+ const results = this._hitTestWorld(mojo_ray_origin, mojo_ray_direction, subscription.entityTypes);
+ frameData.hitTestSubscriptionResults.results.push(
+ {subscriptionId: id, hitTestResults: results});
+ }
+
+ // Transient hit test:
+ const mojo_from_viewer = this._getMojoFromViewer();
+
+ for (const [id, subscription] of this.transientHitTestSubscriptions_) {
+ const result = {subscriptionId: id,
+ inputSourceIdToHitTestResults: new Map()};
+
+ // Find all input sources that match the profile name:
+ const matching_input_sources = Array.from(this.input_sources_.values())
+ .filter(input_source => input_source.profiles_.includes(subscription.profileName));
+
+ for (const input_source of matching_input_sources) {
+ const mojo_from_native_origin = input_source._getMojoFromInputSource(mojo_from_viewer);
+
+ const [mojo_ray_origin, mojo_ray_direction] = this._transformRayToMojoSpace(
+ subscription.ray,
+ mojo_from_native_origin
+ );
+
+ const results = this._hitTestWorld(mojo_ray_origin, mojo_ray_direction, subscription.entityTypes);
+
+ result.inputSourceIdToHitTestResults.set(input_source.source_id_, results);
+ }
+
+ frameData.hitTestSubscriptionResults.transientInputResults.push(result);
+ }
+ }
+
+ // Returns 2-element array [origin, direction] of a ray in mojo space.
+ // |ray| is expressed relative to native origin.
+ _transformRayToMojoSpace(ray, mojo_from_native_origin) {
+ const ray_origin = {
+ x: ray.origin.x,
+ y: ray.origin.y,
+ z: ray.origin.z,
+ w: 1
+ };
+ const ray_direction = {
+ x: ray.direction.x,
+ y: ray.direction.y,
+ z: ray.direction.z,
+ w: 0
+ };
+
+ const mojo_ray_origin = XRMathHelper.transform_by_matrix(
+ mojo_from_native_origin,
+ ray_origin);
+ const mojo_ray_direction = XRMathHelper.transform_by_matrix(
+ mojo_from_native_origin,
+ ray_direction);
+
+ return [mojo_ray_origin, mojo_ray_direction];
+ }
+
+ // Hit tests the passed in ray (expressed as origin and direction) against the mocked world data.
+ _hitTestWorld(origin, direction, entityTypes) {
+ let result = [];
+
+ for (const region of this.world_.hitTestRegions) {
+ const partial_result = this._hitTestRegion(
+ region,
+ origin, direction,
+ entityTypes);
+
+ result = result.concat(partial_result);
+ }
+
+ return result.sort((lhs, rhs) => lhs.distance - rhs.distance).map((hitTest) => {
+ delete hitTest.distance;
+ return hitTest;
+ });
+ }
+
+ // Hit tests the passed in ray (expressed as origin and direction) against world region.
+ // |entityTypes| is a set of FakeXRRegionTypes.
+ // |region| is FakeXRRegion.
+ // Returns array of XRHitResults, each entry will be decorated with the distance from the ray origin (along the ray).
+ _hitTestRegion(region, origin, direction, entityTypes) {
+ const regionNameToMojoEnum = {
+ "point": vrMojom.EntityTypeForHitTest.POINT,
+ "plane": vrMojom.EntityTypeForHitTest.PLANE,
+ "mesh":null
+ };
+
+ if (!entityTypes.includes(regionNameToMojoEnum[region.type])) {
+ return [];
+ }
+
+ const result = [];
+ for (const face of region.faces) {
+ const maybe_hit = this._hitTestFace(face, origin, direction);
+ if (maybe_hit) {
+ result.push(maybe_hit);
+ }
+ }
+
+ // The results should be sorted by distance and there should be no 2 entries with
+ // the same distance from ray origin - that would mean they are the same point.
+ // This situation is possible when a ray intersects the region through an edge shared
+ // by 2 faces.
+ return result.sort((lhs, rhs) => lhs.distance - rhs.distance)
+ .filter((val, index, array) => index === 0 || val.distance !== array[index - 1].distance);
+ }
+
+ // Hit tests the passed in ray (expressed as origin and direction) against a single face.
+ // |face|, |origin|, and |direction| are specified in world (aka mojo) coordinates.
+ // |face| is an array of DOMPointInits.
+ // Returns null if the face does not intersect with the ray, otherwise the result is
+ // an XRHitResult with matrix describing the pose of the intersection point.
+ _hitTestFace(face, origin, direction) {
+ const add = XRMathHelper.add;
+ const sub = XRMathHelper.sub;
+ const mul = XRMathHelper.mul;
+ const normalize = XRMathHelper.normalize;
+ const dot = XRMathHelper.dot;
+ const cross = XRMathHelper.cross;
+ const neg = XRMathHelper.neg;
+
+ //1. Calculate plane normal in world coordinates.
+ const point_A = face.vertices[0];
+ const point_B = face.vertices[1];
+ const point_C = face.vertices[2];
+
+ const edge_AB = sub(point_B, point_A);
+ const edge_AC = sub(point_C, point_A);
+
+ const normal = normalize(cross(edge_AB, edge_AC));
+
+ const numerator = dot(sub(point_A, origin), normal);
+ const denominator = dot(direction, normal);
+
+ if (Math.abs(denominator) < XRMathHelper.EPSILON) {
+ // Planes are nearly parallel - there's either infinitely many intersection points or 0.
+ // Both cases signify a "no hit" for us.
+ return null;
+ } else {
+ // Single intersection point between the infinite plane and the line (*not* ray).
+ // Need to calculate the hit test matrix taking into account the face vertices.
+ const distance = numerator / denominator;
+ if (distance < 0) {
+ // Line - plane intersection exists, but not the half-line - plane does not.
+ return null;
+ } else {
+ const intersection_point = add(origin, mul(distance, direction));
+ // Since we are treating the face as a solid, flip the normal so that its
+ // half-space will contain the ray origin.
+ const y_axis = denominator > 0 ? neg(normal) : normal;
+
+ let z_axis = null;
+ const cos_direction_and_y_axis = dot(direction, y_axis);
+ if (Math.abs(cos_direction_and_y_axis) > (1 - XRMathHelper.EPSILON)) {
+ // Ray and the hit test normal are co-linear - try using the 'up' or 'right' vector's projection on the face plane as the Z axis.
+ // Note: this edge case is currently not covered by the spec.
+ const up = {x: 0.0, y: 1.0, z: 0.0, w: 0.0};
+ const right = {x: 1.0, y: 0.0, z: 0.0, w: 0.0};
+
+ z_axis = Math.abs(dot(up, y_axis)) > (1 - XRMathHelper.EPSILON)
+ ? sub(up, mul(dot(right, y_axis), y_axis)) // `up is also co-linear with hit test normal, use `right`
+ : sub(up, mul(dot(up, y_axis), y_axis)); // `up` is not co-linear with hit test normal, use it
+ } else {
+ // Project the ray direction onto the plane, negate it and use as a Z axis.
+ z_axis = neg(sub(direction, mul(cos_direction_and_y_axis, y_axis))); // Z should point towards the ray origin, not away.
+ }
+
+ z_axis = normalize(z_axis);
+ const x_axis = normalize(cross(y_axis, z_axis));
+
+ // Filter out the points not in polygon.
+ if (!XRMathHelper.pointInFace(intersection_point, face)) {
+ return null;
+ }
+
+ const hitResult = {planeId: 0n};
+ hitResult.distance = distance; // Extend the object with additional information used by higher layers.
+ // It will not be serialized over mojom.
+
+ const matrix = new Array(16);
+
+ matrix[0] = x_axis.x;
+ matrix[1] = x_axis.y;
+ matrix[2] = x_axis.z;
+ matrix[3] = 0;
+
+ matrix[4] = y_axis.x;
+ matrix[5] = y_axis.y;
+ matrix[6] = y_axis.z;
+ matrix[7] = 0;
+
+ matrix[8] = z_axis.x;
+ matrix[9] = z_axis.y;
+ matrix[10] = z_axis.z;
+ matrix[11] = 0;
+
+ matrix[12] = intersection_point.x;
+ matrix[13] = intersection_point.y;
+ matrix[14] = intersection_point.z;
+ matrix[15] = 1;
+
+ hitResult.mojoFromResult = getPoseFromTransform(
+ XRMathHelper.decomposeRigidTransform(matrix));
+ return hitResult;
+ }
+ }
+ }
+
+ _getMojoFromViewer() {
+ if (!this.pose_) {
+ return XRMathHelper.identity();
+ }
+ const transform = {
+ position: [
+ this.pose_.position.x,
+ this.pose_.position.y,
+ this.pose_.position.z],
+ orientation: [
+ this.pose_.orientation.x,
+ this.pose_.orientation.y,
+ this.pose_.orientation.z,
+ this.pose_.orientation.w],
+ };
+
+ return getMatrixFromTransform(transform);
+ }
+
+ _getMojoFromViewerWithOffset(viewOffset) {
+ return { matrix: XRMathHelper.mul4x4(this._getMojoFromViewer(), viewOffset.matrix) };
+ }
+
+ _getMojoFromNativeOrigin(nativeOriginInformation) {
+ const mojo_from_viewer = this._getMojoFromViewer();
+
+ if (nativeOriginInformation.inputSourceSpaceInfo !== undefined) {
+ if (!this.input_sources_.has(nativeOriginInformation.inputSourceSpaceInfo.inputSourceId)) {
+ return null;
+ } else {
+ const inputSource = this.input_sources_.get(nativeOriginInformation.inputSourceSpaceInfo.inputSourceId);
+ return inputSource._getMojoFromInputSource(mojo_from_viewer);
+ }
+ } else if (nativeOriginInformation.referenceSpaceType !== undefined) {
+ switch (nativeOriginInformation.referenceSpaceType) {
+ case vrMojom.XRReferenceSpaceType.kLocal:
+ return XRMathHelper.identity();
+ case vrMojom.XRReferenceSpaceType.kLocalFloor:
+ if (this.stageParameters_ == null || this.stageParameters_.mojoFromFloor == null) {
+ console.warn("Standing transform not available.");
+ return null;
+ }
+ return this.stageParameters_.mojoFromFloor.matrix;
+ case vrMojom.XRReferenceSpaceType.kViewer:
+ return mojo_from_viewer;
+ case vrMojom.XRReferenceSpaceType.kBoundedFloor:
+ return null;
+ case vrMojom.XRReferenceSpaceType.kUnbounded:
+ return null;
+ default:
+ throw new TypeError("Unrecognized XRReferenceSpaceType!");
+ }
+ } else {
+ // Anchors & planes are not yet supported for hit test.
+ return null;
+ }
+ }
+}
+
+class MockXRInputSource {
+ constructor(fakeInputSourceInit, id, pairedDevice) {
+ this.source_id_ = id;
+ this.pairedDevice_ = pairedDevice;
+ this.handedness_ = fakeInputSourceInit.handedness;
+ this.target_ray_mode_ = fakeInputSourceInit.targetRayMode;
+
+ if (fakeInputSourceInit.pointerOrigin == null) {
+ throw new TypeError("FakeXRInputSourceInit.pointerOrigin is required.");
+ }
+
+ this.setPointerOrigin(fakeInputSourceInit.pointerOrigin);
+ this.setProfiles(fakeInputSourceInit.profiles);
+
+ this.primary_input_pressed_ = false;
+ if (fakeInputSourceInit.selectionStarted != null) {
+ this.primary_input_pressed_ = fakeInputSourceInit.selectionStarted;
+ }
+
+ this.primary_input_clicked_ = false;
+ if (fakeInputSourceInit.selectionClicked != null) {
+ this.primary_input_clicked_ = fakeInputSourceInit.selectionClicked;
+ }
+
+ this.primary_squeeze_pressed_ = false;
+ this.primary_squeeze_clicked_ = false;
+
+ this.mojo_from_input_ = null;
+ if (fakeInputSourceInit.gripOrigin != null) {
+ this.setGripOrigin(fakeInputSourceInit.gripOrigin);
+ }
+
+ // This properly handles if supportedButtons were not specified.
+ this.setSupportedButtons(fakeInputSourceInit.supportedButtons);
+
+ this.emulated_position_ = false;
+ this.desc_dirty_ = true;
+ }
+
+ // WebXR Test API
+ setHandedness(handedness) {
+ if (this.handedness_ != handedness) {
+ this.desc_dirty_ = true;
+ this.handedness_ = handedness;
+ }
+ }
+
+ setTargetRayMode(targetRayMode) {
+ if (this.target_ray_mode_ != targetRayMode) {
+ this.desc_dirty_ = true;
+ this.target_ray_mode_ = targetRayMode;
+ }
+ }
+
+ setProfiles(profiles) {
+ this.desc_dirty_ = true;
+ this.profiles_ = profiles;
+ }
+
+ setGripOrigin(transform, emulatedPosition = false) {
+ // grip_origin was renamed to mojo_from_input in mojo
+ this.mojo_from_input_ = composeGFXTransform(transform);
+ this.emulated_position_ = emulatedPosition;
+
+ // Technically, setting the grip shouldn't make the description dirty, but
+ // the webxr-test-api sets our pointer as mojoFromPointer; however, we only
+ // support it across mojom as inputFromPointer, so we need to recalculate it
+ // whenever the grip moves.
+ this.desc_dirty_ = true;
+ }
+
+ clearGripOrigin() {
+ // grip_origin was renamed to mojo_from_input in mojo
+ if (this.mojo_from_input_ != null) {
+ this.mojo_from_input_ = null;
+ this.emulated_position_ = false;
+ this.desc_dirty_ = true;
+ }
+ }
+
+ setPointerOrigin(transform, emulatedPosition = false) {
+ // pointer_origin is mojo_from_pointer.
+ this.desc_dirty_ = true;
+ this.mojo_from_pointer_ = composeGFXTransform(transform);
+ this.emulated_position_ = emulatedPosition;
+ }
+
+ disconnect() {
+ this.pairedDevice_._removeInputSource(this);
+ }
+
+ reconnect() {
+ this.pairedDevice_._addInputSource(this);
+ }
+
+ startSelection() {
+ this.primary_input_pressed_ = true;
+ if (this.gamepad_) {
+ this.gamepad_.buttons[0].pressed = true;
+ this.gamepad_.buttons[0].touched = true;
+ }
+ }
+
+ endSelection() {
+ if (!this.primary_input_pressed_) {
+ throw new Error("Attempted to end selection which was not started");
+ }
+
+ this.primary_input_pressed_ = false;
+ this.primary_input_clicked_ = true;
+
+ if (this.gamepad_) {
+ this.gamepad_.buttons[0].pressed = false;
+ this.gamepad_.buttons[0].touched = false;
+ }
+ }
+
+ simulateSelect() {
+ this.primary_input_clicked_ = true;
+ }
+
+ setSupportedButtons(supportedButtons) {
+ this.gamepad_ = null;
+ this.supported_buttons_ = [];
+
+ // If there are no supported buttons, we can stop now.
+ if (supportedButtons == null || supportedButtons.length < 1) {
+ return;
+ }
+
+ const supported_button_map = {};
+ this.gamepad_ = this._getEmptyGamepad();
+ for (let i = 0; i < supportedButtons.length; i++) {
+ const buttonType = supportedButtons[i].buttonType;
+ this.supported_buttons_.push(buttonType);
+ supported_button_map[buttonType] = supportedButtons[i];
+ }
+
+ // Let's start by building the button state in order of priority:
+ // Primary button is index 0.
+ this.gamepad_.buttons.push({
+ pressed: this.primary_input_pressed_,
+ touched: this.primary_input_pressed_,
+ value: this.primary_input_pressed_ ? 1.0 : 0.0
+ });
+
+ // Now add the rest of our buttons
+ this._addGamepadButton(supported_button_map['grip']);
+ this._addGamepadButton(supported_button_map['touchpad']);
+ this._addGamepadButton(supported_button_map['thumbstick']);
+ this._addGamepadButton(supported_button_map['optional-button']);
+ this._addGamepadButton(supported_button_map['optional-thumbstick']);
+
+ // Finally, back-fill placeholder buttons/axes
+ for (let i = 0; i < this.gamepad_.buttons.length; i++) {
+ if (this.gamepad_.buttons[i] == null) {
+ this.gamepad_.buttons[i] = {
+ pressed: false,
+ touched: false,
+ value: 0
+ };
+ }
+ }
+
+ for (let i=0; i < this.gamepad_.axes.length; i++) {
+ if (this.gamepad_.axes[i] == null) {
+ this.gamepad_.axes[i] = 0;
+ }
+ }
+ }
+
+ updateButtonState(buttonState) {
+ if (this.supported_buttons_.indexOf(buttonState.buttonType) == -1) {
+ throw new Error("Tried to update state on an unsupported button");
+ }
+
+ const buttonIndex = this._getButtonIndex(buttonState.buttonType);
+ const axesStartIndex = this._getAxesStartIndex(buttonState.buttonType);
+
+ if (buttonIndex == -1) {
+ throw new Error("Unknown Button Type!");
+ }
+
+ // is this a 'squeeze' button?
+ if (buttonIndex === this._getButtonIndex('grip')) {
+ // squeeze
+ if (buttonState.pressed) {
+ this.primary_squeeze_pressed_ = true;
+ } else if (this.gamepad_.buttons[buttonIndex].pressed) {
+ this.primary_squeeze_clicked_ = true;
+ this.primary_squeeze_pressed_ = false;
+ } else {
+ this.primary_squeeze_clicked_ = false;
+ this.primary_squeeze_pressed_ = false;
+ }
+ }
+
+ this.gamepad_.buttons[buttonIndex].pressed = buttonState.pressed;
+ this.gamepad_.buttons[buttonIndex].touched = buttonState.touched;
+ this.gamepad_.buttons[buttonIndex].value = buttonState.pressedValue;
+
+ if (axesStartIndex != -1) {
+ this.gamepad_.axes[axesStartIndex] = buttonState.xValue == null ? 0.0 : buttonState.xValue;
+ this.gamepad_.axes[axesStartIndex + 1] = buttonState.yValue == null ? 0.0 : buttonState.yValue;
+ }
+ }
+
+ // DOM Overlay Extensions
+ setOverlayPointerPosition(x, y) {
+ this.overlay_pointer_position_ = {x: x, y: y};
+ }
+
+ // Helpers for Mojom
+ _getInputSourceState() {
+ const input_state = {};
+
+ input_state.sourceId = this.source_id_;
+ input_state.isAuxiliary = false;
+
+ input_state.primaryInputPressed = this.primary_input_pressed_;
+ input_state.primaryInputClicked = this.primary_input_clicked_;
+
+ input_state.primarySqueezePressed = this.primary_squeeze_pressed_;
+ input_state.primarySqueezeClicked = this.primary_squeeze_clicked_;
+ // Setting the input source's "clicked" state should generate one "select"
+ // event. Reset the input value to prevent it from continuously generating
+ // events.
+ this.primary_input_clicked_ = false;
+ // Setting the input source's "clicked" state should generate one "squeeze"
+ // event. Reset the input value to prevent it from continuously generating
+ // events.
+ this.primary_squeeze_clicked_ = false;
+
+ input_state.mojoFromInput = this.mojo_from_input_;
+
+ input_state.gamepad = this.gamepad_;
+
+ input_state.emulatedPosition = this.emulated_position_;
+
+ if (this.desc_dirty_) {
+ const input_desc = {};
+
+ switch (this.target_ray_mode_) {
+ case 'gaze':
+ input_desc.targetRayMode = vrMojom.XRTargetRayMode.GAZING;
+ break;
+ case 'tracked-pointer':
+ input_desc.targetRayMode = vrMojom.XRTargetRayMode.POINTING;
+ break;
+ case 'screen':
+ input_desc.targetRayMode = vrMojom.XRTargetRayMode.TAPPING;
+ break;
+ default:
+ throw new Error('Unhandled target ray mode ' + this.target_ray_mode_);
+ }
+
+ switch (this.handedness_) {
+ case 'left':
+ input_desc.handedness = vrMojom.XRHandedness.LEFT;
+ break;
+ case 'right':
+ input_desc.handedness = vrMojom.XRHandedness.RIGHT;
+ break;
+ default:
+ input_desc.handedness = vrMojom.XRHandedness.NONE;
+ break;
+ }
+
+ // Mojo requires us to send the pointerOrigin as relative to the grip
+ // space. If we don't have a grip space, we'll just assume that there
+ // is a grip at identity. This allows tests to simulate controllers that
+ // are really just a pointer with no tracked grip, though we will end up
+ // exposing that grip space.
+ let mojo_from_input = XRMathHelper.identity();
+ switch (this.target_ray_mode_) {
+ case 'gaze':
+ case 'screen':
+ // For gaze and screen space, we won't have a mojo_from_input; however
+ // the "input" position is just the viewer, so use mojo_from_viewer.
+ mojo_from_input = this.pairedDevice_._getMojoFromViewer();
+ break;
+ case 'tracked-pointer':
+ // If we have a tracked grip position (e.g. mojo_from_input), then use
+ // that. If we don't, then we'll just set the pointer offset directly,
+ // using identity as set above.
+ if (this.mojo_from_input_) {
+ mojo_from_input = this.mojo_from_input_.matrix;
+ }
+ break;
+ default:
+ throw new Error('Unhandled target ray mode ' + this.target_ray_mode_);
+ }
+
+ // To convert mojo_from_pointer to input_from_pointer, we need:
+ // input_from_pointer = input_from_mojo * mojo_from_pointer
+ // Since we store mojo_from_input, we need to invert it here before
+ // multiplying.
+ let input_from_mojo = XRMathHelper.inverse(mojo_from_input);
+ input_desc.inputFromPointer = {};
+ input_desc.inputFromPointer.matrix =
+ XRMathHelper.mul4x4(input_from_mojo, this.mojo_from_pointer_.matrix);
+
+ input_desc.profiles = this.profiles_;
+
+ input_state.description = input_desc;
+
+ this.desc_dirty_ = false;
+ }
+
+ // Pointer data for DOM Overlay, set by setOverlayPointerPosition()
+ if (this.overlay_pointer_position_) {
+ input_state.overlayPointerPosition = this.overlay_pointer_position_;
+ this.overlay_pointer_position_ = null;
+ }
+
+ return input_state;
+ }
+
+ _getEmptyGamepad() {
+ // Mojo complains if some of the properties on Gamepad are null, so set
+ // everything to reasonable defaults that tests can override.
+ const gamepad = {
+ connected: true,
+ id: [],
+ timestamp: 0n,
+ axes: [],
+ buttons: [],
+ touchEvents: [],
+ mapping: GamepadMapping.GamepadMappingStandard,
+ displayId: 0,
+ };
+
+ switch (this.handedness_) {
+ case 'left':
+ gamepad.hand = GamepadHand.GamepadHandLeft;
+ break;
+ case 'right':
+ gamepad.hand = GamepadHand.GamepadHandRight;
+ break;
+ default:
+ gamepad.hand = GamepadHand.GamepadHandNone;
+ break;
+ }
+
+ return gamepad;
+ }
+
+ _addGamepadButton(buttonState) {
+ if (buttonState == null) {
+ return;
+ }
+
+ const buttonIndex = this._getButtonIndex(buttonState.buttonType);
+ const axesStartIndex = this._getAxesStartIndex(buttonState.buttonType);
+
+ if (buttonIndex == -1) {
+ throw new Error("Unknown Button Type!");
+ }
+
+ this.gamepad_.buttons[buttonIndex] = {
+ pressed: buttonState.pressed,
+ touched: buttonState.touched,
+ value: buttonState.pressedValue
+ };
+
+ // Add x/y value if supported.
+ if (axesStartIndex != -1) {
+ this.gamepad_.axes[axesStartIndex] = (buttonState.xValue == null ? 0.0 : buttonSate.xValue);
+ this.gamepad_.axes[axesStartIndex + 1] = (buttonState.yValue == null ? 0.0 : buttonSate.yValue);
+ }
+ }
+
+ // General Helper methods
+ _getButtonIndex(buttonType) {
+ switch (buttonType) {
+ case 'grip':
+ return 1;
+ case 'touchpad':
+ return 2;
+ case 'thumbstick':
+ return 3;
+ case 'optional-button':
+ return 4;
+ case 'optional-thumbstick':
+ return 5;
+ default:
+ return -1;
+ }
+ }
+
+ _getAxesStartIndex(buttonType) {
+ switch (buttonType) {
+ case 'touchpad':
+ return 0;
+ case 'thumbstick':
+ return 2;
+ case 'optional-thumbstick':
+ return 4;
+ default:
+ return -1;
+ }
+ }
+
+ _getMojoFromInputSource(mojo_from_viewer) {
+ return this.mojo_from_pointer_.matrix;
+ }
+}
+
+// Mojo helper classes
+class FakeXRHitTestSourceController {
+ constructor(id) {
+ this.id_ = id;
+ this.deleted_ = false;
+ }
+
+ get deleted() {
+ return this.deleted_;
+ }
+
+ // Internal setter:
+ set deleted(value) {
+ this.deleted_ = value;
+ }
+}
+
+class MockXRPresentationProvider {
+ constructor() {
+ this.receiver_ = null;
+ this.submit_frame_count_ = 0;
+ this.missing_frame_count_ = 0;
+ }
+
+ _bindProvider() {
+ const provider = new vrMojom.XRPresentationProviderRemote();
+
+ if (this.receiver_) {
+ this.receiver_.$.close();
+ }
+ this.receiver_ = new vrMojom.XRPresentationProviderReceiver(this);
+ this.receiver_.$.bindHandle(provider.$.bindNewPipeAndPassReceiver().handle);
+ return provider;
+ }
+
+ _getClientReceiver() {
+ this.submitFrameClient_ = new vrMojom.XRPresentationClientRemote();
+ return this.submitFrameClient_.$.bindNewPipeAndPassReceiver();
+ }
+
+ // XRPresentationProvider mojo implementation
+ updateLayerBounds(frameId, leftBounds, rightBounds, sourceSize) {}
+
+ submitFrameMissing(frameId, mailboxHolder, timeWaited) {
+ this.missing_frame_count_++;
+ }
+
+ submitFrame(frameId, mailboxHolder, timeWaited) {
+ this.submit_frame_count_++;
+
+ // Trigger the submit completion callbacks here. WARNING: The
+ // Javascript-based mojo mocks are *not* re-entrant. It's OK to
+ // wait for these notifications on the next frame, but waiting
+ // within the current frame would never finish since the incoming
+ // calls would be queued until the current execution context finishes.
+ this.submitFrameClient_.onSubmitFrameTransferred(true);
+ this.submitFrameClient_.onSubmitFrameRendered();
+ }
+
+ submitFrameWithTextureHandle(frameId, texture, syncToken) {}
+
+ submitFrameDrawnIntoTexture(frameId, syncToken, timeWaited) {}
+
+ // Utility methods
+ _close() {
+ if (this.receiver_) {
+ this.receiver_.$.close();
+ }
+ }
+}
+
+// Export these into the global object as a side effect of importing this
+// module.
+self.ChromeXRTest = ChromeXRTest;
+self.MockRuntime = MockRuntime;
+self.MockVRService = MockVRService;
+self.SubscribeToHitTestResult = vrMojom.SubscribeToHitTestResult;
+
+navigator.xr.test = new ChromeXRTest();
diff --git a/testing/web-platform/tests/resources/chromium/webxr-test.js.headers b/testing/web-platform/tests/resources/chromium/webxr-test.js.headers
new file mode 100644
index 0000000000..6805c323df
--- /dev/null
+++ b/testing/web-platform/tests/resources/chromium/webxr-test.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8
diff --git a/testing/web-platform/tests/resources/declarative-shadow-dom-polyfill.js b/testing/web-platform/tests/resources/declarative-shadow-dom-polyfill.js
new file mode 100644
index 0000000000..99a3e911eb
--- /dev/null
+++ b/testing/web-platform/tests/resources/declarative-shadow-dom-polyfill.js
@@ -0,0 +1,25 @@
+/*
+ * Polyfill for attaching shadow trees for declarative Shadow DOM for
+ * implementations that do not support declarative Shadow DOM.
+ *
+ * Note: this polyfill will feature-detect the native feature, and do nothing
+ * if supported.
+ *
+ * See: https://github.com/whatwg/html/pull/5465
+ *
+ * root: The root of the subtree in which to upgrade shadow roots
+ *
+ */
+
+function polyfill_declarative_shadow_dom(root) {
+ if (HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode'))
+ return;
+ root.querySelectorAll("template[shadowrootmode]").forEach(template => {
+ const mode = template.getAttribute("shadowrootmode");
+ const delegatesFocus = template.hasAttribute("shadowrootdelegatesfocus");
+ const shadowRoot = template.parentNode.attachShadow({ mode, delegatesFocus });
+ shadowRoot.appendChild(template.content);
+ template.remove();
+ polyfill_declarative_shadow_dom(shadowRoot);
+ });
+}
diff --git a/testing/web-platform/tests/resources/idlharness-shadowrealm.js b/testing/web-platform/tests/resources/idlharness-shadowrealm.js
new file mode 100644
index 0000000000..9484ca6f51
--- /dev/null
+++ b/testing/web-platform/tests/resources/idlharness-shadowrealm.js
@@ -0,0 +1,61 @@
+// TODO: it would be nice to support `idl_array.add_objects`
+function fetch_text(url) {
+ return fetch(url).then(function (r) {
+ if (!r.ok) {
+ throw new Error("Error fetching " + url + ".");
+ }
+ return r.text();
+ });
+}
+
+/**
+ * idl_test_shadowrealm is a promise_test wrapper that handles the fetching of the IDL, and
+ * running the code in a `ShadowRealm`, avoiding repetitive boilerplate.
+ *
+ * @see https://github.com/tc39/proposal-shadowrealm
+ * @param {String[]} srcs Spec name(s) for source idl files (fetched from
+ * /interfaces/{name}.idl).
+ * @param {String[]} deps Spec name(s) for dependency idl files (fetched
+ * from /interfaces/{name}.idl). Order is important - dependencies from
+ * each source will only be included if they're already know to be a
+ * dependency (i.e. have already been seen).
+ */
+function idl_test_shadowrealm(srcs, deps) {
+ promise_setup(async t => {
+ const realm = new ShadowRealm();
+ // https://github.com/web-platform-tests/wpt/issues/31996
+ realm.evaluate("globalThis.self = globalThis; undefined;");
+
+ realm.evaluate(`
+ globalThis.self.GLOBAL = {
+ isWindow: function() { return false; },
+ isWorker: function() { return false; },
+ isShadowRealm: function() { return true; },
+ }; undefined;
+ `);
+ const specs = await Promise.all(srcs.concat(deps).map(spec => {
+ return fetch_text("/interfaces/" + spec + ".idl");
+ }));
+ const idls = JSON.stringify(specs);
+ await new Promise(
+ realm.evaluate(`(resolve,reject) => {
+ (async () => {
+ await import("/resources/testharness.js");
+ await import("/resources/WebIDLParser.js");
+ await import("/resources/idlharness.js");
+ const idls = ${idls};
+ const idl_array = new IdlArray();
+ for (let i = 0; i < ${srcs.length}; i++) {
+ idl_array.add_idls(idls[i]);
+ }
+ for (let i = ${srcs.length}; i < ${srcs.length + deps.length}; i++) {
+ idl_array.add_dependency_idls(idls[i]);
+ }
+ idl_array.test();
+ })().then(resolve, (e) => reject(e.toString()));
+ }`)
+ );
+ await fetch_tests_from_shadow_realm(realm);
+ });
+}
+// vim: set expandtab shiftwidth=4 tabstop=4 foldmarker=@{,@} foldmethod=marker:
diff --git a/testing/web-platform/tests/resources/idlharness.js b/testing/web-platform/tests/resources/idlharness.js
new file mode 100644
index 0000000000..46aa11e5ca
--- /dev/null
+++ b/testing/web-platform/tests/resources/idlharness.js
@@ -0,0 +1,3553 @@
+/* For user documentation see docs/_writing-tests/idlharness.md */
+
+/**
+ * Notes for people who want to edit this file (not just use it as a library):
+ *
+ * Most of the interesting stuff happens in the derived classes of IdlObject,
+ * especially IdlInterface. The entry point for all IdlObjects is .test(),
+ * which is called by IdlArray.test(). An IdlObject is conceptually just
+ * "thing we want to run tests on", and an IdlArray is an array of IdlObjects
+ * with some additional data thrown in.
+ *
+ * The object model is based on what WebIDLParser.js produces, which is in turn
+ * based on its pegjs grammar. If you want to figure out what properties an
+ * object will have from WebIDLParser.js, the best way is to look at the
+ * grammar:
+ *
+ * https://github.com/darobin/webidl.js/blob/master/lib/grammar.peg
+ *
+ * So for instance:
+ *
+ * // interface definition
+ * interface
+ * = extAttrs:extendedAttributeList? S? "interface" S name:identifier w herit:ifInheritance? w "{" w mem:ifMember* w "}" w ";" w
+ * { return { type: "interface", name: name, inheritance: herit, members: mem, extAttrs: extAttrs }; }
+ *
+ * This means that an "interface" object will have a .type property equal to
+ * the string "interface", a .name property equal to the identifier that the
+ * parser found, an .inheritance property equal to either null or the result of
+ * the "ifInheritance" production found elsewhere in the grammar, and so on.
+ * After each grammatical production is a JavaScript function in curly braces
+ * that gets called with suitable arguments and returns some JavaScript value.
+ *
+ * (Note that the version of WebIDLParser.js we use might sometimes be
+ * out-of-date or forked.)
+ *
+ * The members and methods of the classes defined by this file are all at least
+ * briefly documented, hopefully.
+ */
+(function(){
+"use strict";
+// Support subsetTestByKey from /common/subset-tests-by-key.js, but make it optional
+if (!('subsetTestByKey' in self)) {
+ self.subsetTestByKey = function(key, callback, ...args) {
+ return callback(...args);
+ }
+ self.shouldRunSubTest = () => true;
+}
+/// Helpers ///
+function constValue (cnt)
+{
+ if (cnt.type === "null") return null;
+ if (cnt.type === "NaN") return NaN;
+ if (cnt.type === "Infinity") return cnt.negative ? -Infinity : Infinity;
+ if (cnt.type === "number") return +cnt.value;
+ return cnt.value;
+}
+
+function minOverloadLength(overloads)
+{
+ // "The value of the Function object’s “length” property is
+ // a Number determined as follows:
+ // ". . .
+ // "Return the length of the shortest argument list of the
+ // entries in S."
+ if (!overloads.length) {
+ return 0;
+ }
+
+ return overloads.map(function(attr) {
+ return attr.arguments ? attr.arguments.filter(function(arg) {
+ return !arg.optional && !arg.variadic;
+ }).length : 0;
+ })
+ .reduce(function(m, n) { return Math.min(m, n); });
+}
+
+// A helper to get the global of a Function object. This is needed to determine
+// which global exceptions the function throws will come from.
+function globalOf(func)
+{
+ try {
+ // Use the fact that .constructor for a Function object is normally the
+ // Function constructor, which can be used to mint a new function in the
+ // right global.
+ return func.constructor("return this;")();
+ } catch (e) {
+ }
+ // If the above fails, because someone gave us a non-function, or a function
+ // with a weird proto chain or weird .constructor property, just fall back
+ // to 'self'.
+ return self;
+}
+
+// https://esdiscuss.org/topic/isconstructor#content-11
+function isConstructor(o) {
+ try {
+ new (new Proxy(o, {construct: () => ({})}));
+ return true;
+ } catch(e) {
+ return false;
+ }
+}
+
+function throwOrReject(a_test, operation, fn, obj, args, message, cb)
+{
+ if (operation.idlType.generic !== "Promise") {
+ assert_throws_js(globalOf(fn).TypeError, function() {
+ fn.apply(obj, args);
+ }, message);
+ cb();
+ } else {
+ try {
+ promise_rejects_js(a_test, TypeError, fn.apply(obj, args), message).then(cb, cb);
+ } catch (e){
+ a_test.step(function() {
+ assert_unreached("Throws \"" + e + "\" instead of rejecting promise");
+ cb();
+ });
+ }
+ }
+}
+
+function awaitNCallbacks(n, cb, ctx)
+{
+ var counter = 0;
+ return function() {
+ counter++;
+ if (counter >= n) {
+ cb();
+ }
+ };
+}
+
+/// IdlHarnessError ///
+// Entry point
+self.IdlHarnessError = function(message)
+{
+ /**
+ * Message to be printed as the error's toString invocation.
+ */
+ this.message = message;
+};
+
+IdlHarnessError.prototype = Object.create(Error.prototype);
+
+IdlHarnessError.prototype.toString = function()
+{
+ return this.message;
+};
+
+
+/// IdlArray ///
+// Entry point
+self.IdlArray = function()
+{
+ /**
+ * A map from strings to the corresponding named IdlObject, such as
+ * IdlInterface or IdlException. These are the things that test() will run
+ * tests on.
+ */
+ this.members = {};
+
+ /**
+ * A map from strings to arrays of strings. The keys are interface or
+ * exception names, and are expected to also exist as keys in this.members
+ * (otherwise they'll be ignored). This is populated by add_objects() --
+ * see documentation at the start of the file. The actual tests will be
+ * run by calling this.members[name].test_object(obj) for each obj in
+ * this.objects[name]. obj is a string that will be eval'd to produce a
+ * JavaScript value, which is supposed to be an object implementing the
+ * given IdlObject (interface, exception, etc.).
+ */
+ this.objects = {};
+
+ /**
+ * When adding multiple collections of IDLs one at a time, an earlier one
+ * might contain a partial interface or includes statement that depends
+ * on a later one. Save these up and handle them right before we run
+ * tests.
+ *
+ * Both this.partials and this.includes will be the objects as parsed by
+ * WebIDLParser.js, not wrapped in IdlInterface or similar.
+ */
+ this.partials = [];
+ this.includes = [];
+
+ /**
+ * Record of skipped IDL items, in case we later realize that they are a
+ * dependency (to retroactively process them).
+ */
+ this.skipped = new Map();
+};
+
+IdlArray.prototype.add_idls = function(raw_idls, options)
+{
+ /** Entry point. See documentation at beginning of file. */
+ this.internal_add_idls(WebIDL2.parse(raw_idls), options);
+};
+
+IdlArray.prototype.add_untested_idls = function(raw_idls, options)
+{
+ /** Entry point. See documentation at beginning of file. */
+ var parsed_idls = WebIDL2.parse(raw_idls);
+ this.mark_as_untested(parsed_idls);
+ this.internal_add_idls(parsed_idls, options);
+};
+
+IdlArray.prototype.mark_as_untested = function (parsed_idls)
+{
+ for (var i = 0; i < parsed_idls.length; i++) {
+ parsed_idls[i].untested = true;
+ if ("members" in parsed_idls[i]) {
+ for (var j = 0; j < parsed_idls[i].members.length; j++) {
+ parsed_idls[i].members[j].untested = true;
+ }
+ }
+ }
+};
+
+IdlArray.prototype.is_excluded_by_options = function (name, options)
+{
+ return options &&
+ (options.except && options.except.includes(name)
+ || options.only && !options.only.includes(name));
+};
+
+IdlArray.prototype.add_dependency_idls = function(raw_idls, options)
+{
+ return this.internal_add_dependency_idls(WebIDL2.parse(raw_idls), options);
+};
+
+IdlArray.prototype.internal_add_dependency_idls = function(parsed_idls, options)
+{
+ const new_options = { only: [] }
+
+ const all_deps = new Set();
+ Object.values(this.members).forEach(v => {
+ if (v.base) {
+ all_deps.add(v.base);
+ }
+ });
+ // Add both 'A' and 'B' for each 'A includes B' entry.
+ this.includes.forEach(i => {
+ all_deps.add(i.target);
+ all_deps.add(i.includes);
+ });
+ this.partials.forEach(p => all_deps.add(p.name));
+ // Add 'TypeOfType' for each "typedef TypeOfType MyType;" entry.
+ Object.entries(this.members).forEach(([k, v]) => {
+ if (v instanceof IdlTypedef) {
+ let defs = v.idlType.union
+ ? v.idlType.idlType.map(t => t.idlType)
+ : [v.idlType.idlType];
+ defs.forEach(d => all_deps.add(d));
+ }
+ });
+
+ // Add the attribute idlTypes of all the nested members of idls.
+ const attrDeps = parsedIdls => {
+ return parsedIdls.reduce((deps, parsed) => {
+ if (parsed.members) {
+ for (const attr of Object.values(parsed.members).filter(m => m.type === 'attribute')) {
+ let attrType = attr.idlType;
+ // Check for generic members (e.g. FrozenArray<MyType>)
+ if (attrType.generic) {
+ deps.add(attrType.generic);
+ attrType = attrType.idlType;
+ }
+ deps.add(attrType.idlType);
+ }
+ }
+ if (parsed.base in this.members) {
+ attrDeps([this.members[parsed.base]]).forEach(dep => deps.add(dep));
+ }
+ return deps;
+ }, new Set());
+ };
+
+ const testedMembers = Object.values(this.members).filter(m => !m.untested && m.members);
+ attrDeps(testedMembers).forEach(dep => all_deps.add(dep));
+
+ const testedPartials = this.partials.filter(m => !m.untested && m.members);
+ attrDeps(testedPartials).forEach(dep => all_deps.add(dep));
+
+
+ if (options && options.except && options.only) {
+ throw new IdlHarnessError("The only and except options can't be used together.");
+ }
+
+ const defined_or_untested = name => {
+ // NOTE: Deps are untested, so we're lenient, and skip re-encountered definitions.
+ // e.g. for 'idl' containing A:B, B:C, C:D
+ // array.add_idls(idl, {only: ['A','B']}).
+ // array.add_dependency_idls(idl);
+ // B would be encountered as tested, and encountered as a dep, so we ignore.
+ return name in this.members
+ || this.is_excluded_by_options(name, options);
+ }
+ // Maps name -> [parsed_idl, ...]
+ const process = function(parsed) {
+ var deps = [];
+ if (parsed.name) {
+ deps.push(parsed.name);
+ } else if (parsed.type === "includes") {
+ deps.push(parsed.target);
+ deps.push(parsed.includes);
+ }
+
+ deps = deps.filter(function(name) {
+ if (!name
+ || name === parsed.name && defined_or_untested(name)
+ || !all_deps.has(name)) {
+ // Flag as skipped, if it's not already processed, so we can
+ // come back to it later if we retrospectively call it a dep.
+ if (name && !(name in this.members)) {
+ this.skipped.has(name)
+ ? this.skipped.get(name).push(parsed)
+ : this.skipped.set(name, [parsed]);
+ }
+ return false;
+ }
+ return true;
+ }.bind(this));
+
+ deps.forEach(function(name) {
+ if (!new_options.only.includes(name)) {
+ new_options.only.push(name);
+ }
+
+ const follow_up = new Set();
+ for (const dep_type of ["inheritance", "includes"]) {
+ if (parsed[dep_type]) {
+ const inheriting = parsed[dep_type];
+ const inheritor = parsed.name || parsed.target;
+ const deps = [inheriting];
+ // For A includes B, we can ignore A, unless B (or some of its
+ // members) is being tested.
+ if (dep_type !== "includes"
+ || inheriting in this.members && !this.members[inheriting].untested
+ || this.partials.some(function(p) {
+ return p.name === inheriting;
+ })) {
+ deps.push(inheritor);
+ }
+ for (const dep of deps) {
+ if (!new_options.only.includes(dep)) {
+ new_options.only.push(dep);
+ }
+ all_deps.add(dep);
+ follow_up.add(dep);
+ }
+ }
+ }
+
+ for (const deferred of follow_up) {
+ if (this.skipped.has(deferred)) {
+ const next = this.skipped.get(deferred);
+ this.skipped.delete(deferred);
+ next.forEach(process);
+ }
+ }
+ }.bind(this));
+ }.bind(this);
+
+ for (let parsed of parsed_idls) {
+ process(parsed);
+ }
+
+ this.mark_as_untested(parsed_idls);
+
+ if (new_options.only.length) {
+ this.internal_add_idls(parsed_idls, new_options);
+ }
+}
+
+IdlArray.prototype.internal_add_idls = function(parsed_idls, options)
+{
+ /**
+ * Internal helper called by add_idls() and add_untested_idls().
+ *
+ * parsed_idls is an array of objects that come from WebIDLParser.js's
+ * "definitions" production. The add_untested_idls() entry point
+ * additionally sets an .untested property on each object (and its
+ * .members) so that they'll be skipped by test() -- they'll only be
+ * used for base interfaces of tested interfaces, return types, etc.
+ *
+ * options is a dictionary that can have an only or except member which are
+ * arrays. If only is given then only members, partials and interface
+ * targets listed will be added, and if except is given only those that
+ * aren't listed will be added. Only one of only and except can be used.
+ */
+
+ if (options && options.only && options.except)
+ {
+ throw new IdlHarnessError("The only and except options can't be used together.");
+ }
+
+ var should_skip = name => {
+ return this.is_excluded_by_options(name, options);
+ }
+
+ parsed_idls.forEach(function(parsed_idl)
+ {
+ var partial_types = [
+ "interface",
+ "interface mixin",
+ "dictionary",
+ "namespace",
+ ];
+ if (parsed_idl.partial && partial_types.includes(parsed_idl.type))
+ {
+ if (should_skip(parsed_idl.name))
+ {
+ return;
+ }
+ this.partials.push(parsed_idl);
+ return;
+ }
+
+ if (parsed_idl.type == "includes")
+ {
+ if (should_skip(parsed_idl.target))
+ {
+ return;
+ }
+ this.includes.push(parsed_idl);
+ return;
+ }
+
+ parsed_idl.array = this;
+ if (should_skip(parsed_idl.name))
+ {
+ return;
+ }
+ if (parsed_idl.name in this.members)
+ {
+ throw new IdlHarnessError("Duplicate identifier " + parsed_idl.name);
+ }
+
+ switch(parsed_idl.type)
+ {
+ case "interface":
+ this.members[parsed_idl.name] =
+ new IdlInterface(parsed_idl, /* is_callback = */ false, /* is_mixin = */ false);
+ break;
+
+ case "interface mixin":
+ this.members[parsed_idl.name] =
+ new IdlInterface(parsed_idl, /* is_callback = */ false, /* is_mixin = */ true);
+ break;
+
+ case "dictionary":
+ // Nothing to test, but we need the dictionary info around for type
+ // checks
+ this.members[parsed_idl.name] = new IdlDictionary(parsed_idl);
+ break;
+
+ case "typedef":
+ this.members[parsed_idl.name] = new IdlTypedef(parsed_idl);
+ break;
+
+ case "callback":
+ this.members[parsed_idl.name] = new IdlCallback(parsed_idl);
+ break;
+
+ case "enum":
+ this.members[parsed_idl.name] = new IdlEnum(parsed_idl);
+ break;
+
+ case "callback interface":
+ this.members[parsed_idl.name] =
+ new IdlInterface(parsed_idl, /* is_callback = */ true, /* is_mixin = */ false);
+ break;
+
+ case "namespace":
+ this.members[parsed_idl.name] = new IdlNamespace(parsed_idl);
+ break;
+
+ default:
+ throw parsed_idl.name + ": " + parsed_idl.type + " not yet supported";
+ }
+ }.bind(this));
+};
+
+IdlArray.prototype.add_objects = function(dict)
+{
+ /** Entry point. See documentation at beginning of file. */
+ for (var k in dict)
+ {
+ if (k in this.objects)
+ {
+ this.objects[k] = this.objects[k].concat(dict[k]);
+ }
+ else
+ {
+ this.objects[k] = dict[k];
+ }
+ }
+};
+
+IdlArray.prototype.prevent_multiple_testing = function(name)
+{
+ /** Entry point. See documentation at beginning of file. */
+ this.members[name].prevent_multiple_testing = true;
+};
+
+IdlArray.prototype.is_json_type = function(type)
+{
+ /**
+ * Checks whether type is a JSON type as per
+ * https://webidl.spec.whatwg.org/#dfn-json-types
+ */
+
+ var idlType = type.idlType;
+
+ if (type.generic == "Promise") { return false; }
+
+ // nullable and annotated types don't need to be handled separately,
+ // as webidl2 doesn't represent them wrapped-up (as they're described
+ // in WebIDL).
+
+ // union and record types
+ if (type.union || type.generic == "record") {
+ return idlType.every(this.is_json_type, this);
+ }
+
+ // sequence types
+ if (type.generic == "sequence" || type.generic == "FrozenArray") {
+ return this.is_json_type(idlType[0]);
+ }
+
+ if (typeof idlType != "string") { throw new Error("Unexpected type " + JSON.stringify(idlType)); }
+
+ switch (idlType)
+ {
+ // Numeric types
+ case "byte":
+ case "octet":
+ case "short":
+ case "unsigned short":
+ case "long":
+ case "unsigned long":
+ case "long long":
+ case "unsigned long long":
+ case "float":
+ case "double":
+ case "unrestricted float":
+ case "unrestricted double":
+ // boolean
+ case "boolean":
+ // string types
+ case "DOMString":
+ case "ByteString":
+ case "USVString":
+ // object type
+ case "object":
+ return true;
+ case "Error":
+ case "DOMException":
+ case "Int8Array":
+ case "Int16Array":
+ case "Int32Array":
+ case "Uint8Array":
+ case "Uint16Array":
+ case "Uint32Array":
+ case "Uint8ClampedArray":
+ case "BigInt64Array":
+ case "BigUint64Array":
+ case "Float32Array":
+ case "Float64Array":
+ case "ArrayBuffer":
+ case "DataView":
+ case "any":
+ return false;
+ default:
+ var thing = this.members[idlType];
+ if (!thing) { throw new Error("Type " + idlType + " not found"); }
+ if (thing instanceof IdlEnum) { return true; }
+
+ if (thing instanceof IdlTypedef) {
+ return this.is_json_type(thing.idlType);
+ }
+
+ // dictionaries where all of their members are JSON types
+ if (thing instanceof IdlDictionary) {
+ const map = new Map();
+ for (const dict of thing.get_reverse_inheritance_stack()) {
+ for (const m of dict.members) {
+ map.set(m.name, m.idlType);
+ }
+ }
+ return Array.from(map.values()).every(this.is_json_type, this);
+ }
+
+ // interface types that have a toJSON operation declared on themselves or
+ // one of their inherited interfaces.
+ if (thing instanceof IdlInterface) {
+ var base;
+ while (thing)
+ {
+ if (thing.has_to_json_regular_operation()) { return true; }
+ var mixins = this.includes[thing.name];
+ if (mixins) {
+ mixins = mixins.map(function(id) {
+ var mixin = this.members[id];
+ if (!mixin) {
+ throw new Error("Interface " + id + " not found (implemented by " + thing.name + ")");
+ }
+ return mixin;
+ }, this);
+ if (mixins.some(function(m) { return m.has_to_json_regular_operation() } )) { return true; }
+ }
+ if (!thing.base) { return false; }
+ base = this.members[thing.base];
+ if (!base) {
+ throw new Error("Interface " + thing.base + " not found (inherited by " + thing.name + ")");
+ }
+ thing = base;
+ }
+ return false;
+ }
+ return false;
+ }
+};
+
+function exposure_set(object, default_set) {
+ var exposed = object.extAttrs && object.extAttrs.filter(a => a.name === "Exposed");
+ if (exposed && exposed.length > 1) {
+ throw new IdlHarnessError(
+ `Multiple 'Exposed' extended attributes on ${object.name}`);
+ }
+
+ let result = default_set || ["Window"];
+ if (result && !(result instanceof Set)) {
+ result = new Set(result);
+ }
+ if (exposed && exposed.length) {
+ const { rhs } = exposed[0];
+ // Could be a list or a string.
+ const set =
+ rhs.type === "*" ?
+ [ "*" ] :
+ rhs.type === "identifier-list" ?
+ rhs.value.map(id => id.value) :
+ [ rhs.value ];
+ result = new Set(set);
+ }
+ if (result && result.has("*")) {
+ return "*";
+ }
+ if (result && result.has("Worker")) {
+ result.delete("Worker");
+ result.add("DedicatedWorker");
+ result.add("ServiceWorker");
+ result.add("SharedWorker");
+ }
+ return result;
+}
+
+function exposed_in(globals) {
+ if (globals === "*") {
+ return true;
+ }
+ if ('Window' in self) {
+ return globals.has("Window");
+ }
+ if ('DedicatedWorkerGlobalScope' in self &&
+ self instanceof DedicatedWorkerGlobalScope) {
+ return globals.has("DedicatedWorker");
+ }
+ if ('SharedWorkerGlobalScope' in self &&
+ self instanceof SharedWorkerGlobalScope) {
+ return globals.has("SharedWorker");
+ }
+ if ('ServiceWorkerGlobalScope' in self &&
+ self instanceof ServiceWorkerGlobalScope) {
+ return globals.has("ServiceWorker");
+ }
+ if (Object.getPrototypeOf(self) === Object.prototype) {
+ // ShadowRealm - only exposed with `"*"`.
+ return false;
+ }
+ throw new IdlHarnessError("Unexpected global object");
+}
+
+/**
+ * Asserts that the given error message is thrown for the given function.
+ * @param {string|IdlHarnessError} error Expected Error message.
+ * @param {Function} idlArrayFunc Function operating on an IdlArray that should throw.
+ */
+IdlArray.prototype.assert_throws = function(error, idlArrayFunc)
+{
+ try {
+ idlArrayFunc.call(this, this);
+ } catch (e) {
+ if (e instanceof AssertionError) {
+ throw e;
+ }
+ // Assertions for behaviour of the idlharness.js engine.
+ if (error instanceof IdlHarnessError) {
+ error = error.message;
+ }
+ if (e.message !== error) {
+ throw new IdlHarnessError(`${idlArrayFunc} threw "${e}", not the expected IdlHarnessError "${error}"`);
+ }
+ return;
+ }
+ throw new IdlHarnessError(`${idlArrayFunc} did not throw the expected IdlHarnessError`);
+}
+
+IdlArray.prototype.test = function()
+{
+ /** Entry point. See documentation at beginning of file. */
+
+ // First merge in all partial definitions and interface mixins.
+ this.merge_partials();
+ this.merge_mixins();
+
+ // Assert B defined for A : B
+ for (const member of Object.values(this.members).filter(m => m.base)) {
+ const lhs = member.name;
+ const rhs = member.base;
+ if (!(rhs in this.members)) throw new IdlHarnessError(`${lhs} inherits ${rhs}, but ${rhs} is undefined.`);
+ const lhs_is_interface = this.members[lhs] instanceof IdlInterface;
+ const rhs_is_interface = this.members[rhs] instanceof IdlInterface;
+ if (rhs_is_interface != lhs_is_interface) {
+ if (!lhs_is_interface) throw new IdlHarnessError(`${lhs} inherits ${rhs}, but ${lhs} is not an interface.`);
+ if (!rhs_is_interface) throw new IdlHarnessError(`${lhs} inherits ${rhs}, but ${rhs} is not an interface.`);
+ }
+ // Check for circular dependencies.
+ member.get_reverse_inheritance_stack();
+ }
+
+ Object.getOwnPropertyNames(this.members).forEach(function(memberName) {
+ var member = this.members[memberName];
+ if (!(member instanceof IdlInterface)) {
+ return;
+ }
+
+ var globals = exposure_set(member);
+ member.exposed = exposed_in(globals);
+ member.exposureSet = globals;
+ }.bind(this));
+
+ // Now run test() on every member, and test_object() for every object.
+ for (var name in this.members)
+ {
+ this.members[name].test();
+ if (name in this.objects)
+ {
+ const objects = this.objects[name];
+ if (!objects || !Array.isArray(objects)) {
+ throw new IdlHarnessError(`Invalid or empty objects for member ${name}`);
+ }
+ objects.forEach(function(str)
+ {
+ if (!this.members[name] || !(this.members[name] instanceof IdlInterface)) {
+ throw new IdlHarnessError(`Invalid object member name ${name}`);
+ }
+ this.members[name].test_object(str);
+ }.bind(this));
+ }
+ }
+};
+
+IdlArray.prototype.merge_partials = function()
+{
+ const testedPartials = new Map();
+ this.partials.forEach(function(parsed_idl)
+ {
+ const originalExists = parsed_idl.name in this.members
+ && (this.members[parsed_idl.name] instanceof IdlInterface
+ || this.members[parsed_idl.name] instanceof IdlDictionary
+ || this.members[parsed_idl.name] instanceof IdlNamespace);
+
+ // Ensure unique test name in case of multiple partials.
+ let partialTestName = parsed_idl.name;
+ let partialTestCount = 1;
+ if (testedPartials.has(parsed_idl.name)) {
+ partialTestCount += testedPartials.get(parsed_idl.name);
+ partialTestName = `${partialTestName}[${partialTestCount}]`;
+ }
+ testedPartials.set(parsed_idl.name, partialTestCount);
+
+ if (!parsed_idl.untested) {
+ test(function () {
+ assert_true(originalExists, `Original ${parsed_idl.type} should be defined`);
+
+ var expected;
+ switch (parsed_idl.type) {
+ case 'dictionary': expected = IdlDictionary; break;
+ case 'namespace': expected = IdlNamespace; break;
+ case 'interface':
+ case 'interface mixin':
+ default:
+ expected = IdlInterface; break;
+ }
+ assert_true(
+ expected.prototype.isPrototypeOf(this.members[parsed_idl.name]),
+ `Original ${parsed_idl.name} definition should have type ${parsed_idl.type}`);
+ }.bind(this), `Partial ${parsed_idl.type} ${partialTestName}: original ${parsed_idl.type} defined`);
+ }
+ if (!originalExists) {
+ // Not good.. but keep calm and carry on.
+ return;
+ }
+
+ if (parsed_idl.extAttrs)
+ {
+ // Special-case "Exposed". Must be a subset of original interface's exposure.
+ // Exposed on a partial is the equivalent of having the same Exposed on all nested members.
+ // See https://github.com/heycam/webidl/issues/154 for discrepency between Exposed and
+ // other extended attributes on partial interfaces.
+ const exposureAttr = parsed_idl.extAttrs.find(a => a.name === "Exposed");
+ if (exposureAttr) {
+ if (!parsed_idl.untested) {
+ test(function () {
+ const partialExposure = exposure_set(parsed_idl);
+ const memberExposure = exposure_set(this.members[parsed_idl.name]);
+ if (memberExposure === "*") {
+ return;
+ }
+ if (partialExposure === "*") {
+ throw new IdlHarnessError(
+ `Partial ${parsed_idl.name} ${parsed_idl.type} is exposed everywhere, the original ${parsed_idl.type} is not.`);
+ }
+ partialExposure.forEach(name => {
+ if (!memberExposure || !memberExposure.has(name)) {
+ throw new IdlHarnessError(
+ `Partial ${parsed_idl.name} ${parsed_idl.type} is exposed to '${name}', the original ${parsed_idl.type} is not.`);
+ }
+ });
+ }.bind(this), `Partial ${parsed_idl.type} ${partialTestName}: valid exposure set`);
+ }
+ parsed_idl.members.forEach(function (member) {
+ member.extAttrs.push(exposureAttr);
+ }.bind(this));
+ }
+
+ parsed_idl.extAttrs.forEach(function(extAttr)
+ {
+ // "Exposed" already handled above.
+ if (extAttr.name === "Exposed") {
+ return;
+ }
+ this.members[parsed_idl.name].extAttrs.push(extAttr);
+ }.bind(this));
+ }
+ if (parsed_idl.members.length) {
+ test(function () {
+ var clash = parsed_idl.members.find(function(member) {
+ return this.members[parsed_idl.name].members.find(function(m) {
+ return this.are_duplicate_members(m, member);
+ }.bind(this));
+ }.bind(this));
+ parsed_idl.members.forEach(function(member)
+ {
+ this.members[parsed_idl.name].members.push(new IdlInterfaceMember(member));
+ }.bind(this));
+ assert_true(!clash, "member " + (clash && clash.name) + " is unique");
+ }.bind(this), `Partial ${parsed_idl.type} ${partialTestName}: member names are unique`);
+ }
+ }.bind(this));
+ this.partials = [];
+}
+
+IdlArray.prototype.merge_mixins = function()
+{
+ for (const parsed_idl of this.includes)
+ {
+ const lhs = parsed_idl.target;
+ const rhs = parsed_idl.includes;
+
+ var errStr = lhs + " includes " + rhs + ", but ";
+ if (!(lhs in this.members)) throw errStr + lhs + " is undefined.";
+ if (!(this.members[lhs] instanceof IdlInterface)) throw errStr + lhs + " is not an interface.";
+ if (!(rhs in this.members)) throw errStr + rhs + " is undefined.";
+ if (!(this.members[rhs] instanceof IdlInterface)) throw errStr + rhs + " is not an interface.";
+
+ if (this.members[rhs].members.length) {
+ test(function () {
+ var clash = this.members[rhs].members.find(function(member) {
+ return this.members[lhs].members.find(function(m) {
+ return this.are_duplicate_members(m, member);
+ }.bind(this));
+ }.bind(this));
+ this.members[rhs].members.forEach(function(member) {
+ assert_true(
+ this.members[lhs].members.every(m => !this.are_duplicate_members(m, member)),
+ "member " + member.name + " is unique");
+ this.members[lhs].members.push(new IdlInterfaceMember(member));
+ }.bind(this));
+ assert_true(!clash, "member " + (clash && clash.name) + " is unique");
+ }.bind(this), lhs + " includes " + rhs + ": member names are unique");
+ }
+ }
+ this.includes = [];
+}
+
+IdlArray.prototype.are_duplicate_members = function(m1, m2) {
+ if (m1.name !== m2.name) {
+ return false;
+ }
+ if (m1.type === 'operation' && m2.type === 'operation'
+ && m1.arguments.length !== m2.arguments.length) {
+ // Method overload. TODO: Deep comparison of arguments.
+ return false;
+ }
+ return true;
+}
+
+IdlArray.prototype.assert_type_is = function(value, type)
+{
+ if (type.idlType in this.members
+ && this.members[type.idlType] instanceof IdlTypedef) {
+ this.assert_type_is(value, this.members[type.idlType].idlType);
+ return;
+ }
+
+ if (type.nullable && value === null)
+ {
+ // This is fine
+ return;
+ }
+
+ if (type.union) {
+ for (var i = 0; i < type.idlType.length; i++) {
+ try {
+ this.assert_type_is(value, type.idlType[i]);
+ // No AssertionError, so we match one type in the union
+ return;
+ } catch(e) {
+ if (e instanceof AssertionError) {
+ // We didn't match this type, let's try some others
+ continue;
+ }
+ throw e;
+ }
+ }
+ // TODO: Is there a nice way to list the union's types in the message?
+ assert_true(false, "Attribute has value " + format_value(value)
+ + " which doesn't match any of the types in the union");
+
+ }
+
+ /**
+ * Helper function that tests that value is an instance of type according
+ * to the rules of WebIDL. value is any JavaScript value, and type is an
+ * object produced by WebIDLParser.js' "type" production. That production
+ * is fairly elaborate due to the complexity of WebIDL's types, so it's
+ * best to look at the grammar to figure out what properties it might have.
+ */
+ if (type.idlType == "any")
+ {
+ // No assertions to make
+ return;
+ }
+
+ if (type.array)
+ {
+ // TODO: not supported yet
+ return;
+ }
+
+ if (type.generic === "sequence" || type.generic == "ObservableArray")
+ {
+ assert_true(Array.isArray(value), "should be an Array");
+ if (!value.length)
+ {
+ // Nothing we can do.
+ return;
+ }
+ this.assert_type_is(value[0], type.idlType[0]);
+ return;
+ }
+
+ if (type.generic === "Promise") {
+ assert_true("then" in value, "Attribute with a Promise type should have a then property");
+ // TODO: Ideally, we would check on project fulfillment
+ // that we get the right type
+ // but that would require making the type check async
+ return;
+ }
+
+ if (type.generic === "FrozenArray") {
+ assert_true(Array.isArray(value), "Value should be array");
+ assert_true(Object.isFrozen(value), "Value should be frozen");
+ if (!value.length)
+ {
+ // Nothing we can do.
+ return;
+ }
+ this.assert_type_is(value[0], type.idlType[0]);
+ return;
+ }
+
+ type = Array.isArray(type.idlType) ? type.idlType[0] : type.idlType;
+
+ switch(type)
+ {
+ case "undefined":
+ assert_equals(value, undefined);
+ return;
+
+ case "boolean":
+ assert_equals(typeof value, "boolean");
+ return;
+
+ case "byte":
+ assert_equals(typeof value, "number");
+ assert_equals(value, Math.floor(value), "should be an integer");
+ assert_true(-128 <= value && value <= 127, "byte " + value + " should be in range [-128, 127]");
+ return;
+
+ case "octet":
+ assert_equals(typeof value, "number");
+ assert_equals(value, Math.floor(value), "should be an integer");
+ assert_true(0 <= value && value <= 255, "octet " + value + " should be in range [0, 255]");
+ return;
+
+ case "short":
+ assert_equals(typeof value, "number");
+ assert_equals(value, Math.floor(value), "should be an integer");
+ assert_true(-32768 <= value && value <= 32767, "short " + value + " should be in range [-32768, 32767]");
+ return;
+
+ case "unsigned short":
+ assert_equals(typeof value, "number");
+ assert_equals(value, Math.floor(value), "should be an integer");
+ assert_true(0 <= value && value <= 65535, "unsigned short " + value + " should be in range [0, 65535]");
+ return;
+
+ case "long":
+ assert_equals(typeof value, "number");
+ assert_equals(value, Math.floor(value), "should be an integer");
+ assert_true(-2147483648 <= value && value <= 2147483647, "long " + value + " should be in range [-2147483648, 2147483647]");
+ return;
+
+ case "unsigned long":
+ assert_equals(typeof value, "number");
+ assert_equals(value, Math.floor(value), "should be an integer");
+ assert_true(0 <= value && value <= 4294967295, "unsigned long " + value + " should be in range [0, 4294967295]");
+ return;
+
+ case "long long":
+ assert_equals(typeof value, "number");
+ return;
+
+ case "unsigned long long":
+ case "DOMTimeStamp":
+ assert_equals(typeof value, "number");
+ assert_true(0 <= value, "unsigned long long should be positive");
+ return;
+
+ case "float":
+ assert_equals(typeof value, "number");
+ assert_equals(value, Math.fround(value), "float rounded to 32-bit float should be itself");
+ assert_not_equals(value, Infinity);
+ assert_not_equals(value, -Infinity);
+ assert_not_equals(value, NaN);
+ return;
+
+ case "DOMHighResTimeStamp":
+ case "double":
+ assert_equals(typeof value, "number");
+ assert_not_equals(value, Infinity);
+ assert_not_equals(value, -Infinity);
+ assert_not_equals(value, NaN);
+ return;
+
+ case "unrestricted float":
+ assert_equals(typeof value, "number");
+ assert_equals(value, Math.fround(value), "unrestricted float rounded to 32-bit float should be itself");
+ return;
+
+ case "unrestricted double":
+ assert_equals(typeof value, "number");
+ return;
+
+ case "DOMString":
+ assert_equals(typeof value, "string");
+ return;
+
+ case "ByteString":
+ assert_equals(typeof value, "string");
+ assert_regexp_match(value, /^[\x00-\x7F]*$/);
+ return;
+
+ case "USVString":
+ assert_equals(typeof value, "string");
+ assert_regexp_match(value, /^([\x00-\ud7ff\ue000-\uffff]|[\ud800-\udbff][\udc00-\udfff])*$/);
+ return;
+
+ case "ArrayBufferView":
+ assert_true(ArrayBuffer.isView(value));
+ return;
+
+ case "object":
+ assert_in_array(typeof value, ["object", "function"], "wrong type: not object or function");
+ return;
+ }
+
+ // This is a catch-all for any IDL type name which follows JS class
+ // semantics. This includes some non-interface IDL types (e.g. Int8Array,
+ // Function, ...), as well as any interface types that are not in the IDL
+ // that is fed to the harness. If an IDL type does not follow JS class
+ // semantics then it should go in the switch statement above. If an IDL
+ // type needs full checking, then the test should include it in the IDL it
+ // feeds to the harness.
+ if (!(type in this.members))
+ {
+ assert_true(value instanceof self[type], "wrong type: not a " + type);
+ return;
+ }
+
+ if (this.members[type] instanceof IdlInterface)
+ {
+ // We don't want to run the full
+ // IdlInterface.prototype.test_instance_of, because that could result
+ // in an infinite loop. TODO: This means we don't have tests for
+ // LegacyNoInterfaceObject interfaces, and we also can't test objects
+ // that come from another self.
+ assert_in_array(typeof value, ["object", "function"], "wrong type: not object or function");
+ if (value instanceof Object
+ && !this.members[type].has_extended_attribute("LegacyNoInterfaceObject")
+ && type in self)
+ {
+ assert_true(value instanceof self[type], "instanceof " + type);
+ }
+ }
+ else if (this.members[type] instanceof IdlEnum)
+ {
+ assert_equals(typeof value, "string");
+ }
+ else if (this.members[type] instanceof IdlDictionary)
+ {
+ // TODO: Test when we actually have something to test this on
+ }
+ else if (this.members[type] instanceof IdlCallback)
+ {
+ assert_equals(typeof value, "function");
+ }
+ else
+ {
+ throw new IdlHarnessError("Type " + type + " isn't an interface, callback or dictionary");
+ }
+};
+
+/// IdlObject ///
+function IdlObject() {}
+IdlObject.prototype.test = function()
+{
+ /**
+ * By default, this does nothing, so no actual tests are run for IdlObjects
+ * that don't define any (e.g., IdlDictionary at the time of this writing).
+ */
+};
+
+IdlObject.prototype.has_extended_attribute = function(name)
+{
+ /**
+ * This is only meaningful for things that support extended attributes,
+ * such as interfaces, exceptions, and members.
+ */
+ return this.extAttrs.some(function(o)
+ {
+ return o.name == name;
+ });
+};
+
+
+/// IdlDictionary ///
+// Used for IdlArray.prototype.assert_type_is
+function IdlDictionary(obj)
+{
+ /**
+ * obj is an object produced by the WebIDLParser.js "dictionary"
+ * production.
+ */
+
+ /** Self-explanatory. */
+ this.name = obj.name;
+
+ /** A back-reference to our IdlArray. */
+ this.array = obj.array;
+
+ /** An array of objects produced by the "dictionaryMember" production. */
+ this.members = obj.members;
+
+ /**
+ * The name (as a string) of the dictionary type we inherit from, or null
+ * if there is none.
+ */
+ this.base = obj.inheritance;
+}
+
+IdlDictionary.prototype = Object.create(IdlObject.prototype);
+
+IdlDictionary.prototype.get_reverse_inheritance_stack = function() {
+ return IdlInterface.prototype.get_reverse_inheritance_stack.call(this);
+};
+
+/// IdlInterface ///
+function IdlInterface(obj, is_callback, is_mixin)
+{
+ /**
+ * obj is an object produced by the WebIDLParser.js "interface" production.
+ */
+
+ /** Self-explanatory. */
+ this.name = obj.name;
+
+ /** A back-reference to our IdlArray. */
+ this.array = obj.array;
+
+ /**
+ * An indicator of whether we should run tests on the interface object and
+ * interface prototype object. Tests on members are controlled by .untested
+ * on each member, not this.
+ */
+ this.untested = obj.untested;
+
+ /** An array of objects produced by the "ExtAttr" production. */
+ this.extAttrs = obj.extAttrs;
+
+ /** An array of IdlInterfaceMembers. */
+ this.members = obj.members.map(function(m){return new IdlInterfaceMember(m); });
+ if (this.has_extended_attribute("LegacyUnforgeable")) {
+ this.members
+ .filter(function(m) { return m.special !== "static" && (m.type == "attribute" || m.type == "operation"); })
+ .forEach(function(m) { return m.isUnforgeable = true; });
+ }
+
+ /**
+ * The name (as a string) of the type we inherit from, or null if there is
+ * none.
+ */
+ this.base = obj.inheritance;
+
+ this._is_callback = is_callback;
+ this._is_mixin = is_mixin;
+}
+IdlInterface.prototype = Object.create(IdlObject.prototype);
+IdlInterface.prototype.is_callback = function()
+{
+ return this._is_callback;
+};
+
+IdlInterface.prototype.is_mixin = function()
+{
+ return this._is_mixin;
+};
+
+IdlInterface.prototype.has_constants = function()
+{
+ return this.members.some(function(member) {
+ return member.type === "const";
+ });
+};
+
+IdlInterface.prototype.get_unscopables = function()
+{
+ return this.members.filter(function(member) {
+ return member.isUnscopable;
+ });
+};
+
+IdlInterface.prototype.is_global = function()
+{
+ return this.extAttrs.some(function(attribute) {
+ return attribute.name === "Global";
+ });
+};
+
+/**
+ * Value of the LegacyNamespace extended attribute, if any.
+ *
+ * https://webidl.spec.whatwg.org/#LegacyNamespace
+ */
+IdlInterface.prototype.get_legacy_namespace = function()
+{
+ var legacyNamespace = this.extAttrs.find(function(attribute) {
+ return attribute.name === "LegacyNamespace";
+ });
+ return legacyNamespace ? legacyNamespace.rhs.value : undefined;
+};
+
+IdlInterface.prototype.get_interface_object_owner = function()
+{
+ var legacyNamespace = this.get_legacy_namespace();
+ return legacyNamespace ? self[legacyNamespace] : self;
+};
+
+IdlInterface.prototype.should_have_interface_object = function()
+{
+ // "For every interface that is exposed in a given ECMAScript global
+ // environment and:
+ // * is a callback interface that has constants declared on it, or
+ // * is a non-callback interface that is not declared with the
+ // [LegacyNoInterfaceObject] extended attribute,
+ // a corresponding property MUST exist on the ECMAScript global object.
+
+ return this.is_callback() ? this.has_constants() : !this.has_extended_attribute("LegacyNoInterfaceObject");
+};
+
+IdlInterface.prototype.assert_interface_object_exists = function()
+{
+ var owner = this.get_legacy_namespace() || "self";
+ assert_own_property(self[owner], this.name, owner + " does not have own property " + format_value(this.name));
+};
+
+IdlInterface.prototype.get_interface_object = function() {
+ if (!this.should_have_interface_object()) {
+ var reason = this.is_callback() ? "lack of declared constants" : "declared [LegacyNoInterfaceObject] attribute";
+ throw new IdlHarnessError(this.name + " has no interface object due to " + reason);
+ }
+
+ return this.get_interface_object_owner()[this.name];
+};
+
+IdlInterface.prototype.get_qualified_name = function() {
+ // https://webidl.spec.whatwg.org/#qualified-name
+ var legacyNamespace = this.get_legacy_namespace();
+ if (legacyNamespace) {
+ return legacyNamespace + "." + this.name;
+ }
+ return this.name;
+};
+
+IdlInterface.prototype.has_to_json_regular_operation = function() {
+ return this.members.some(function(m) {
+ return m.is_to_json_regular_operation();
+ });
+};
+
+IdlInterface.prototype.has_default_to_json_regular_operation = function() {
+ return this.members.some(function(m) {
+ return m.is_to_json_regular_operation() && m.has_extended_attribute("Default");
+ });
+};
+
+/**
+ * Implementation of https://webidl.spec.whatwg.org/#create-an-inheritance-stack
+ * with the order reversed.
+ *
+ * The order is reversed so that the base class comes first in the list, because
+ * this is what all call sites need.
+ *
+ * So given:
+ *
+ * A : B {};
+ * B : C {};
+ * C {};
+ *
+ * then A.get_reverse_inheritance_stack() returns [C, B, A],
+ * and B.get_reverse_inheritance_stack() returns [C, B].
+ *
+ * Note: as dictionary inheritance is expressed identically by the AST,
+ * this works just as well for getting a stack of inherited dictionaries.
+ */
+IdlInterface.prototype.get_reverse_inheritance_stack = function() {
+ const stack = [this];
+ let idl_interface = this;
+ while (idl_interface.base) {
+ const base = this.array.members[idl_interface.base];
+ if (!base) {
+ throw new Error(idl_interface.type + " " + idl_interface.base + " not found (inherited by " + idl_interface.name + ")");
+ } else if (stack.indexOf(base) > -1) {
+ stack.unshift(base);
+ const dep_chain = stack.map(i => i.name).join(',');
+ throw new IdlHarnessError(`${this.name} has a circular dependency: ${dep_chain}`);
+ }
+ idl_interface = base;
+ stack.unshift(idl_interface);
+ }
+ return stack;
+};
+
+/**
+ * Implementation of
+ * https://webidl.spec.whatwg.org/#default-tojson-operation
+ * for testing purposes.
+ *
+ * Collects the IDL types of the attributes that meet the criteria
+ * for inclusion in the default toJSON operation for easy
+ * comparison with actual value
+ */
+IdlInterface.prototype.default_to_json_operation = function() {
+ const map = new Map()
+ let isDefault = false;
+ for (const I of this.get_reverse_inheritance_stack()) {
+ if (I.has_default_to_json_regular_operation()) {
+ isDefault = true;
+ for (const m of I.members) {
+ if (m.special !== "static" && m.type == "attribute" && I.array.is_json_type(m.idlType)) {
+ map.set(m.name, m.idlType);
+ }
+ }
+ } else if (I.has_to_json_regular_operation()) {
+ isDefault = false;
+ }
+ }
+ return isDefault ? map : null;
+};
+
+IdlInterface.prototype.test = function()
+{
+ if (this.has_extended_attribute("LegacyNoInterfaceObject") || this.is_mixin())
+ {
+ // No tests to do without an instance. TODO: We should still be able
+ // to run tests on the prototype object, if we obtain one through some
+ // other means.
+ return;
+ }
+
+ // If the interface object is not exposed, only test that. Members can't be
+ // tested either, but objects could still be tested in |test_object|.
+ if (!this.exposed)
+ {
+ if (!this.untested)
+ {
+ subsetTestByKey(this.name, test, function() {
+ assert_false(this.name in self);
+ }.bind(this), this.name + " interface: existence and properties of interface object");
+ }
+ return;
+ }
+
+ if (!this.untested)
+ {
+ // First test things to do with the exception/interface object and
+ // exception/interface prototype object.
+ this.test_self();
+ }
+ // Then test things to do with its members (constants, fields, attributes,
+ // operations, . . .). These are run even if .untested is true, because
+ // members might themselves be marked as .untested. This might happen to
+ // interfaces if the interface itself is untested but a partial interface
+ // that extends it is tested -- then the interface itself and its initial
+ // members will be marked as untested, but the members added by the partial
+ // interface are still tested.
+ this.test_members();
+};
+
+IdlInterface.prototype.constructors = function()
+{
+ return this.members
+ .filter(function(m) { return m.type == "constructor"; });
+}
+
+IdlInterface.prototype.test_self = function()
+{
+ subsetTestByKey(this.name, test, function()
+ {
+ if (!this.should_have_interface_object()) {
+ return;
+ }
+
+ // The name of the property is the identifier of the interface, and its
+ // value is an object called the interface object.
+ // The property has the attributes { [[Writable]]: true,
+ // [[Enumerable]]: false, [[Configurable]]: true }."
+ // TODO: Should we test here that the property is actually writable
+ // etc., or trust getOwnPropertyDescriptor?
+ this.assert_interface_object_exists();
+ var desc = Object.getOwnPropertyDescriptor(this.get_interface_object_owner(), this.name);
+ assert_false("get" in desc, "self's property " + format_value(this.name) + " should not have a getter");
+ assert_false("set" in desc, "self's property " + format_value(this.name) + " should not have a setter");
+ assert_true(desc.writable, "self's property " + format_value(this.name) + " should be writable");
+ assert_false(desc.enumerable, "self's property " + format_value(this.name) + " should not be enumerable");
+ assert_true(desc.configurable, "self's property " + format_value(this.name) + " should be configurable");
+
+ if (this.is_callback()) {
+ // "The internal [[Prototype]] property of an interface object for
+ // a callback interface must be the Function.prototype object."
+ assert_equals(Object.getPrototypeOf(this.get_interface_object()), Function.prototype,
+ "prototype of self's property " + format_value(this.name) + " is not Object.prototype");
+
+ return;
+ }
+
+ // "The interface object for a given non-callback interface is a
+ // function object."
+ // "If an object is defined to be a function object, then it has
+ // characteristics as follows:"
+
+ // Its [[Prototype]] internal property is otherwise specified (see
+ // below).
+
+ // "* Its [[Get]] internal property is set as described in ECMA-262
+ // section 9.1.8."
+ // Not much to test for this.
+
+ // "* Its [[Construct]] internal property is set as described in
+ // ECMA-262 section 19.2.2.3."
+
+ // "* Its @@hasInstance property is set as described in ECMA-262
+ // section 19.2.3.8, unless otherwise specified."
+ // TODO
+
+ // ES6 (rev 30) 19.1.3.6:
+ // "Else, if O has a [[Call]] internal method, then let builtinTag be
+ // "Function"."
+ assert_class_string(this.get_interface_object(), "Function", "class string of " + this.name);
+
+ // "The [[Prototype]] internal property of an interface object for a
+ // non-callback interface is determined as follows:"
+ var prototype = Object.getPrototypeOf(this.get_interface_object());
+ if (this.base) {
+ // "* If the interface inherits from some other interface, the
+ // value of [[Prototype]] is the interface object for that other
+ // interface."
+ var inherited_interface = this.array.members[this.base];
+ if (!inherited_interface.has_extended_attribute("LegacyNoInterfaceObject")) {
+ inherited_interface.assert_interface_object_exists();
+ assert_equals(prototype, inherited_interface.get_interface_object(),
+ 'prototype of ' + this.name + ' is not ' +
+ this.base);
+ }
+ } else {
+ // "If the interface doesn't inherit from any other interface, the
+ // value of [[Prototype]] is %FunctionPrototype% ([ECMA-262],
+ // section 6.1.7.4)."
+ assert_equals(prototype, Function.prototype,
+ "prototype of self's property " + format_value(this.name) + " is not Function.prototype");
+ }
+
+ // Always test for [[Construct]]:
+ // https://github.com/heycam/webidl/issues/698
+ assert_true(isConstructor(this.get_interface_object()), "interface object must pass IsConstructor check");
+
+ var interface_object = this.get_interface_object();
+ assert_throws_js(globalOf(interface_object).TypeError, function() {
+ interface_object();
+ }, "interface object didn't throw TypeError when called as a function");
+
+ if (!this.constructors().length) {
+ assert_throws_js(globalOf(interface_object).TypeError, function() {
+ new interface_object();
+ }, "interface object didn't throw TypeError when called as a constructor");
+ }
+ }.bind(this), this.name + " interface: existence and properties of interface object");
+
+ if (this.should_have_interface_object() && !this.is_callback()) {
+ subsetTestByKey(this.name, test, function() {
+ // This function tests WebIDL as of 2014-10-25.
+ // https://webidl.spec.whatwg.org/#es-interface-call
+
+ this.assert_interface_object_exists();
+
+ // "Interface objects for non-callback interfaces MUST have a
+ // property named “length” with attributes { [[Writable]]: false,
+ // [[Enumerable]]: false, [[Configurable]]: true } whose value is
+ // a Number."
+ assert_own_property(this.get_interface_object(), "length");
+ var desc = Object.getOwnPropertyDescriptor(this.get_interface_object(), "length");
+ assert_false("get" in desc, this.name + ".length should not have a getter");
+ assert_false("set" in desc, this.name + ".length should not have a setter");
+ assert_false(desc.writable, this.name + ".length should not be writable");
+ assert_false(desc.enumerable, this.name + ".length should not be enumerable");
+ assert_true(desc.configurable, this.name + ".length should be configurable");
+
+ var constructors = this.constructors();
+ var expected_length = minOverloadLength(constructors);
+ assert_equals(this.get_interface_object().length, expected_length, "wrong value for " + this.name + ".length");
+ }.bind(this), this.name + " interface object length");
+ }
+
+ if (this.should_have_interface_object()) {
+ subsetTestByKey(this.name, test, function() {
+ // This function tests WebIDL as of 2015-11-17.
+ // https://webidl.spec.whatwg.org/#interface-object
+
+ this.assert_interface_object_exists();
+
+ // "All interface objects must have a property named “name” with
+ // attributes { [[Writable]]: false, [[Enumerable]]: false,
+ // [[Configurable]]: true } whose value is the identifier of the
+ // corresponding interface."
+
+ assert_own_property(this.get_interface_object(), "name");
+ var desc = Object.getOwnPropertyDescriptor(this.get_interface_object(), "name");
+ assert_false("get" in desc, this.name + ".name should not have a getter");
+ assert_false("set" in desc, this.name + ".name should not have a setter");
+ assert_false(desc.writable, this.name + ".name should not be writable");
+ assert_false(desc.enumerable, this.name + ".name should not be enumerable");
+ assert_true(desc.configurable, this.name + ".name should be configurable");
+ assert_equals(this.get_interface_object().name, this.name, "wrong value for " + this.name + ".name");
+ }.bind(this), this.name + " interface object name");
+ }
+
+
+ if (this.has_extended_attribute("LegacyWindowAlias")) {
+ subsetTestByKey(this.name, test, function()
+ {
+ var aliasAttrs = this.extAttrs.filter(function(o) { return o.name === "LegacyWindowAlias"; });
+ if (aliasAttrs.length > 1) {
+ throw new IdlHarnessError("Invalid IDL: multiple LegacyWindowAlias extended attributes on " + this.name);
+ }
+ if (this.is_callback()) {
+ throw new IdlHarnessError("Invalid IDL: LegacyWindowAlias extended attribute on non-interface " + this.name);
+ }
+ if (!(this.exposureSet === "*" || this.exposureSet.has("Window"))) {
+ throw new IdlHarnessError("Invalid IDL: LegacyWindowAlias extended attribute on " + this.name + " which is not exposed in Window");
+ }
+ // TODO: when testing of [LegacyNoInterfaceObject] interfaces is supported,
+ // check that it's not specified together with LegacyWindowAlias.
+
+ // TODO: maybe check that [LegacyWindowAlias] is not specified on a partial interface.
+
+ var rhs = aliasAttrs[0].rhs;
+ if (!rhs) {
+ throw new IdlHarnessError("Invalid IDL: LegacyWindowAlias extended attribute on " + this.name + " without identifier");
+ }
+ var aliases;
+ if (rhs.type === "identifier-list") {
+ aliases = rhs.value.map(id => id.value);
+ } else { // rhs.type === identifier
+ aliases = [ rhs.value ];
+ }
+
+ // OK now actually check the aliases...
+ var alias;
+ if (exposed_in(exposure_set(this, this.exposureSet)) && 'document' in self) {
+ for (alias of aliases) {
+ assert_true(alias in self, alias + " should exist");
+ assert_equals(self[alias], this.get_interface_object(), "self." + alias + " should be the same value as self." + this.get_qualified_name());
+ var desc = Object.getOwnPropertyDescriptor(self, alias);
+ assert_equals(desc.value, this.get_interface_object(), "wrong value in " + alias + " property descriptor");
+ assert_true(desc.writable, alias + " should be writable");
+ assert_false(desc.enumerable, alias + " should not be enumerable");
+ assert_true(desc.configurable, alias + " should be configurable");
+ assert_false('get' in desc, alias + " should not have a getter");
+ assert_false('set' in desc, alias + " should not have a setter");
+ }
+ } else {
+ for (alias of aliases) {
+ assert_false(alias in self, alias + " should not exist");
+ }
+ }
+
+ }.bind(this), this.name + " interface: legacy window alias");
+ }
+
+ if (this.has_extended_attribute("LegacyFactoryFunction")) {
+ var constructors = this.extAttrs
+ .filter(function(attr) { return attr.name == "LegacyFactoryFunction"; });
+ if (constructors.length !== 1) {
+ throw new IdlHarnessError("Internal error: missing support for multiple LegacyFactoryFunction extended attributes");
+ }
+ var constructor = constructors[0];
+ var min_length = minOverloadLength([constructor]);
+
+ subsetTestByKey(this.name, test, function()
+ {
+ // This function tests WebIDL as of 2019-01-14.
+
+ // "for every [LegacyFactoryFunction] extended attribute on an exposed
+ // interface, a corresponding property must exist on the ECMAScript
+ // global object. The name of the property is the
+ // [LegacyFactoryFunction]'s identifier, and its value is an object
+ // called a named constructor, ... . The property has the attributes
+ // { [[Writable]]: true, [[Enumerable]]: false,
+ // [[Configurable]]: true }."
+ var name = constructor.rhs.value;
+ assert_own_property(self, name);
+ var desc = Object.getOwnPropertyDescriptor(self, name);
+ assert_equals(desc.value, self[name], "wrong value in " + name + " property descriptor");
+ assert_true(desc.writable, name + " should be writable");
+ assert_false(desc.enumerable, name + " should not be enumerable");
+ assert_true(desc.configurable, name + " should be configurable");
+ assert_false("get" in desc, name + " should not have a getter");
+ assert_false("set" in desc, name + " should not have a setter");
+ }.bind(this), this.name + " interface: named constructor");
+
+ subsetTestByKey(this.name, test, function()
+ {
+ // This function tests WebIDL as of 2019-01-14.
+
+ // "2. Let F be ! CreateBuiltinFunction(realm, steps,
+ // realm.[[Intrinsics]].[[%FunctionPrototype%]])."
+ var name = constructor.rhs.value;
+ var value = self[name];
+ assert_equals(typeof value, "function", "type of value in " + name + " property descriptor");
+ assert_not_equals(value, this.get_interface_object(), "wrong value in " + name + " property descriptor");
+ assert_equals(Object.getPrototypeOf(value), Function.prototype, "wrong value for " + name + "'s prototype");
+ }.bind(this), this.name + " interface: named constructor object");
+
+ subsetTestByKey(this.name, test, function()
+ {
+ // This function tests WebIDL as of 2019-01-14.
+
+ // "7. Let proto be the interface prototype object of interface I
+ // in realm.
+ // "8. Perform ! DefinePropertyOrThrow(F, "prototype",
+ // PropertyDescriptor{
+ // [[Value]]: proto, [[Writable]]: false,
+ // [[Enumerable]]: false, [[Configurable]]: false
+ // })."
+ var name = constructor.rhs.value;
+ var expected = this.get_interface_object().prototype;
+ var desc = Object.getOwnPropertyDescriptor(self[name], "prototype");
+ assert_equals(desc.value, expected, "wrong value for " + name + ".prototype");
+ assert_false(desc.writable, "prototype should not be writable");
+ assert_false(desc.enumerable, "prototype should not be enumerable");
+ assert_false(desc.configurable, "prototype should not be configurable");
+ assert_false("get" in desc, "prototype should not have a getter");
+ assert_false("set" in desc, "prototype should not have a setter");
+ }.bind(this), this.name + " interface: named constructor prototype property");
+
+ subsetTestByKey(this.name, test, function()
+ {
+ // This function tests WebIDL as of 2019-01-14.
+
+ // "3. Perform ! SetFunctionName(F, id)."
+ var name = constructor.rhs.value;
+ var desc = Object.getOwnPropertyDescriptor(self[name], "name");
+ assert_equals(desc.value, name, "wrong value for " + name + ".name");
+ assert_false(desc.writable, "name should not be writable");
+ assert_false(desc.enumerable, "name should not be enumerable");
+ assert_true(desc.configurable, "name should be configurable");
+ assert_false("get" in desc, "name should not have a getter");
+ assert_false("set" in desc, "name should not have a setter");
+ }.bind(this), this.name + " interface: named constructor name");
+
+ subsetTestByKey(this.name, test, function()
+ {
+ // This function tests WebIDL as of 2019-01-14.
+
+ // "4. Initialize S to the effective overload set for constructors
+ // with identifier id on interface I and with argument count 0.
+ // "5. Let length be the length of the shortest argument list of
+ // the entries in S.
+ // "6. Perform ! SetFunctionLength(F, length)."
+ var name = constructor.rhs.value;
+ var desc = Object.getOwnPropertyDescriptor(self[name], "length");
+ assert_equals(desc.value, min_length, "wrong value for " + name + ".length");
+ assert_false(desc.writable, "length should not be writable");
+ assert_false(desc.enumerable, "length should not be enumerable");
+ assert_true(desc.configurable, "length should be configurable");
+ assert_false("get" in desc, "length should not have a getter");
+ assert_false("set" in desc, "length should not have a setter");
+ }.bind(this), this.name + " interface: named constructor length");
+
+ subsetTestByKey(this.name, test, function()
+ {
+ // This function tests WebIDL as of 2019-01-14.
+
+ // "1. Let steps be the following steps:
+ // " 1. If NewTarget is undefined, then throw a TypeError."
+ var name = constructor.rhs.value;
+ var args = constructor.arguments.map(function(arg) {
+ return create_suitable_object(arg.idlType);
+ });
+ assert_throws_js(globalOf(self[name]).TypeError, function() {
+ self[name](...args);
+ }.bind(this));
+ }.bind(this), this.name + " interface: named constructor without 'new'");
+ }
+
+ subsetTestByKey(this.name, test, function()
+ {
+ // This function tests WebIDL as of 2015-01-21.
+ // https://webidl.spec.whatwg.org/#interface-object
+
+ if (!this.should_have_interface_object()) {
+ return;
+ }
+
+ this.assert_interface_object_exists();
+
+ if (this.is_callback()) {
+ assert_false("prototype" in this.get_interface_object(),
+ this.name + ' should not have a "prototype" property');
+ return;
+ }
+
+ // "An interface object for a non-callback interface must have a
+ // property named “prototype” with attributes { [[Writable]]: false,
+ // [[Enumerable]]: false, [[Configurable]]: false } whose value is an
+ // object called the interface prototype object. This object has
+ // properties that correspond to the regular attributes and regular
+ // operations defined on the interface, and is described in more detail
+ // in section 4.5.4 below."
+ assert_own_property(this.get_interface_object(), "prototype",
+ 'interface "' + this.name + '" does not have own property "prototype"');
+ var desc = Object.getOwnPropertyDescriptor(this.get_interface_object(), "prototype");
+ assert_false("get" in desc, this.name + ".prototype should not have a getter");
+ assert_false("set" in desc, this.name + ".prototype should not have a setter");
+ assert_false(desc.writable, this.name + ".prototype should not be writable");
+ assert_false(desc.enumerable, this.name + ".prototype should not be enumerable");
+ assert_false(desc.configurable, this.name + ".prototype should not be configurable");
+
+ // Next, test that the [[Prototype]] of the interface prototype object
+ // is correct. (This is made somewhat difficult by the existence of
+ // [LegacyNoInterfaceObject].)
+ // TODO: Aryeh thinks there's at least other place in this file where
+ // we try to figure out if an interface prototype object is
+ // correct. Consolidate that code.
+
+ // "The interface prototype object for a given interface A must have an
+ // internal [[Prototype]] property whose value is returned from the
+ // following steps:
+ // "If A is declared with the [Global] extended
+ // attribute, and A supports named properties, then return the named
+ // properties object for A, as defined in §3.6.4 Named properties
+ // object.
+ // "Otherwise, if A is declared to inherit from another interface, then
+ // return the interface prototype object for the inherited interface.
+ // "Otherwise, return %ObjectPrototype%.
+ //
+ // "In the ECMAScript binding, the DOMException type has some additional
+ // requirements:
+ //
+ // "Unlike normal interface types, the interface prototype object
+ // for DOMException must have as its [[Prototype]] the intrinsic
+ // object %ErrorPrototype%."
+ //
+ if (this.name === "Window") {
+ assert_class_string(Object.getPrototypeOf(this.get_interface_object().prototype),
+ 'WindowProperties',
+ 'Class name for prototype of Window' +
+ '.prototype is not "WindowProperties"');
+ } else {
+ var inherit_interface, inherit_interface_interface_object;
+ if (this.base) {
+ inherit_interface = this.base;
+ var parent = this.array.members[inherit_interface];
+ if (!parent.has_extended_attribute("LegacyNoInterfaceObject")) {
+ parent.assert_interface_object_exists();
+ inherit_interface_interface_object = parent.get_interface_object();
+ }
+ } else if (this.name === "DOMException") {
+ inherit_interface = 'Error';
+ inherit_interface_interface_object = self.Error;
+ } else {
+ inherit_interface = 'Object';
+ inherit_interface_interface_object = self.Object;
+ }
+ if (inherit_interface_interface_object) {
+ assert_not_equals(inherit_interface_interface_object, undefined,
+ 'should inherit from ' + inherit_interface + ', but there is no such property');
+ assert_own_property(inherit_interface_interface_object, 'prototype',
+ 'should inherit from ' + inherit_interface + ', but that object has no "prototype" property');
+ assert_equals(Object.getPrototypeOf(this.get_interface_object().prototype),
+ inherit_interface_interface_object.prototype,
+ 'prototype of ' + this.name + '.prototype is not ' + inherit_interface + '.prototype');
+ } else {
+ // We can't test that we get the correct object, because this is the
+ // only way to get our hands on it. We only test that its class
+ // string, at least, is correct.
+ assert_class_string(Object.getPrototypeOf(this.get_interface_object().prototype),
+ inherit_interface + 'Prototype',
+ 'Class name for prototype of ' + this.name +
+ '.prototype is not "' + inherit_interface + 'Prototype"');
+ }
+ }
+
+ // "The class string of an interface prototype object is the
+ // concatenation of the interface’s qualified identifier and the string
+ // “Prototype”."
+
+ // Skip these tests for now due to a specification issue about
+ // prototype name.
+ // https://www.w3.org/Bugs/Public/show_bug.cgi?id=28244
+
+ // assert_class_string(this.get_interface_object().prototype, this.get_qualified_name() + "Prototype",
+ // "class string of " + this.name + ".prototype");
+
+ // String() should end up calling {}.toString if nothing defines a
+ // stringifier.
+ if (!this.has_stringifier()) {
+ // assert_equals(String(this.get_interface_object().prototype), "[object " + this.get_qualified_name() + "Prototype]",
+ // "String(" + this.name + ".prototype)");
+ }
+ }.bind(this), this.name + " interface: existence and properties of interface prototype object");
+
+ // "If the interface is declared with the [Global]
+ // extended attribute, or the interface is in the set of inherited
+ // interfaces for any other interface that is declared with one of these
+ // attributes, then the interface prototype object must be an immutable
+ // prototype exotic object."
+ // https://webidl.spec.whatwg.org/#interface-prototype-object
+ if (this.is_global()) {
+ this.test_immutable_prototype("interface prototype object", this.get_interface_object().prototype);
+ }
+
+ subsetTestByKey(this.name, test, function()
+ {
+ if (!this.should_have_interface_object()) {
+ return;
+ }
+
+ this.assert_interface_object_exists();
+
+ if (this.is_callback()) {
+ assert_false("prototype" in this.get_interface_object(),
+ this.name + ' should not have a "prototype" property');
+ return;
+ }
+
+ assert_own_property(this.get_interface_object(), "prototype",
+ 'interface "' + this.name + '" does not have own property "prototype"');
+
+ // "If the [LegacyNoInterfaceObject] extended attribute was not specified
+ // on the interface, then the interface prototype object must also have a
+ // property named “constructor” with attributes { [[Writable]]: true,
+ // [[Enumerable]]: false, [[Configurable]]: true } whose value is a
+ // reference to the interface object for the interface."
+ assert_own_property(this.get_interface_object().prototype, "constructor",
+ this.name + '.prototype does not have own property "constructor"');
+ var desc = Object.getOwnPropertyDescriptor(this.get_interface_object().prototype, "constructor");
+ assert_false("get" in desc, this.name + ".prototype.constructor should not have a getter");
+ assert_false("set" in desc, this.name + ".prototype.constructor should not have a setter");
+ assert_true(desc.writable, this.name + ".prototype.constructor should be writable");
+ assert_false(desc.enumerable, this.name + ".prototype.constructor should not be enumerable");
+ assert_true(desc.configurable, this.name + ".prototype.constructor should be configurable");
+ assert_equals(this.get_interface_object().prototype.constructor, this.get_interface_object(),
+ this.name + '.prototype.constructor is not the same object as ' + this.name);
+ }.bind(this), this.name + ' interface: existence and properties of interface prototype object\'s "constructor" property');
+
+
+ subsetTestByKey(this.name, test, function()
+ {
+ if (!this.should_have_interface_object()) {
+ return;
+ }
+
+ this.assert_interface_object_exists();
+
+ if (this.is_callback()) {
+ assert_false("prototype" in this.get_interface_object(),
+ this.name + ' should not have a "prototype" property');
+ return;
+ }
+
+ assert_own_property(this.get_interface_object(), "prototype",
+ 'interface "' + this.name + '" does not have own property "prototype"');
+
+ // If the interface has any member declared with the [Unscopable] extended
+ // attribute, then there must be a property on the interface prototype object
+ // whose name is the @@unscopables symbol, which has the attributes
+ // { [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: true },
+ // and whose value is an object created as follows...
+ var unscopables = this.get_unscopables().map(m => m.name);
+ var proto = this.get_interface_object().prototype;
+ if (unscopables.length != 0) {
+ assert_own_property(
+ proto, Symbol.unscopables,
+ this.name + '.prototype should have an @@unscopables property');
+ var desc = Object.getOwnPropertyDescriptor(proto, Symbol.unscopables);
+ assert_false("get" in desc,
+ this.name + ".prototype[Symbol.unscopables] should not have a getter");
+ assert_false("set" in desc, this.name + ".prototype[Symbol.unscopables] should not have a setter");
+ assert_false(desc.writable, this.name + ".prototype[Symbol.unscopables] should not be writable");
+ assert_false(desc.enumerable, this.name + ".prototype[Symbol.unscopables] should not be enumerable");
+ assert_true(desc.configurable, this.name + ".prototype[Symbol.unscopables] should be configurable");
+ assert_equals(desc.value, proto[Symbol.unscopables],
+ this.name + '.prototype[Symbol.unscopables] should be in the descriptor');
+ assert_equals(typeof desc.value, "object",
+ this.name + '.prototype[Symbol.unscopables] should be an object');
+ assert_equals(Object.getPrototypeOf(desc.value), null,
+ this.name + '.prototype[Symbol.unscopables] should have a null prototype');
+ assert_equals(Object.getOwnPropertySymbols(desc.value).length,
+ 0,
+ this.name + '.prototype[Symbol.unscopables] should have the right number of symbol-named properties');
+
+ // Check that we do not have _extra_ unscopables. Checking that we
+ // have all the ones we should will happen in the per-member tests.
+ var observed = Object.getOwnPropertyNames(desc.value);
+ for (var prop of observed) {
+ assert_not_equals(unscopables.indexOf(prop),
+ -1,
+ this.name + '.prototype[Symbol.unscopables] has unexpected property "' + prop + '"');
+ }
+ } else {
+ assert_equals(Object.getOwnPropertyDescriptor(this.get_interface_object().prototype, Symbol.unscopables),
+ undefined,
+ this.name + '.prototype should not have @@unscopables');
+ }
+ }.bind(this), this.name + ' interface: existence and properties of interface prototype object\'s @@unscopables property');
+};
+
+IdlInterface.prototype.test_immutable_prototype = function(type, obj)
+{
+ if (typeof Object.setPrototypeOf !== "function") {
+ return;
+ }
+
+ subsetTestByKey(this.name, test, function(t) {
+ var originalValue = Object.getPrototypeOf(obj);
+ var newValue = Object.create(null);
+
+ t.add_cleanup(function() {
+ try {
+ Object.setPrototypeOf(obj, originalValue);
+ } catch (err) {}
+ });
+
+ assert_throws_js(TypeError, function() {
+ Object.setPrototypeOf(obj, newValue);
+ });
+
+ assert_equals(
+ Object.getPrototypeOf(obj),
+ originalValue,
+ "original value not modified"
+ );
+ }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " +
+ "of " + type + " - setting to a new value via Object.setPrototypeOf " +
+ "should throw a TypeError");
+
+ subsetTestByKey(this.name, test, function(t) {
+ var originalValue = Object.getPrototypeOf(obj);
+ var newValue = Object.create(null);
+
+ t.add_cleanup(function() {
+ let setter = Object.getOwnPropertyDescriptor(
+ Object.prototype, '__proto__'
+ ).set;
+
+ try {
+ setter.call(obj, originalValue);
+ } catch (err) {}
+ });
+
+ // We need to find the actual setter for the '__proto__' property, so we
+ // can determine the right global for it. Walk up the prototype chain
+ // looking for that property until we find it.
+ let setter;
+ {
+ let cur = obj;
+ while (cur) {
+ const desc = Object.getOwnPropertyDescriptor(cur, "__proto__");
+ if (desc) {
+ setter = desc.set;
+ break;
+ }
+ cur = Object.getPrototypeOf(cur);
+ }
+ }
+ assert_throws_js(globalOf(setter).TypeError, function() {
+ obj.__proto__ = newValue;
+ });
+
+ assert_equals(
+ Object.getPrototypeOf(obj),
+ originalValue,
+ "original value not modified"
+ );
+ }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " +
+ "of " + type + " - setting to a new value via __proto__ " +
+ "should throw a TypeError");
+
+ subsetTestByKey(this.name, test, function(t) {
+ var originalValue = Object.getPrototypeOf(obj);
+ var newValue = Object.create(null);
+
+ t.add_cleanup(function() {
+ try {
+ Reflect.setPrototypeOf(obj, originalValue);
+ } catch (err) {}
+ });
+
+ assert_false(Reflect.setPrototypeOf(obj, newValue));
+
+ assert_equals(
+ Object.getPrototypeOf(obj),
+ originalValue,
+ "original value not modified"
+ );
+ }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " +
+ "of " + type + " - setting to a new value via Reflect.setPrototypeOf " +
+ "should return false");
+
+ subsetTestByKey(this.name, test, function() {
+ var originalValue = Object.getPrototypeOf(obj);
+
+ Object.setPrototypeOf(obj, originalValue);
+ }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " +
+ "of " + type + " - setting to its original value via Object.setPrototypeOf " +
+ "should not throw");
+
+ subsetTestByKey(this.name, test, function() {
+ var originalValue = Object.getPrototypeOf(obj);
+
+ obj.__proto__ = originalValue;
+ }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " +
+ "of " + type + " - setting to its original value via __proto__ " +
+ "should not throw");
+
+ subsetTestByKey(this.name, test, function() {
+ var originalValue = Object.getPrototypeOf(obj);
+
+ assert_true(Reflect.setPrototypeOf(obj, originalValue));
+ }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " +
+ "of " + type + " - setting to its original value via Reflect.setPrototypeOf " +
+ "should return true");
+};
+
+IdlInterface.prototype.test_member_const = function(member)
+{
+ if (!this.has_constants()) {
+ throw new IdlHarnessError("Internal error: test_member_const called without any constants");
+ }
+
+ subsetTestByKey(this.name, test, function()
+ {
+ this.assert_interface_object_exists();
+
+ // "For each constant defined on an interface A, there must be
+ // a corresponding property on the interface object, if it
+ // exists."
+ assert_own_property(this.get_interface_object(), member.name);
+ // "The value of the property is that which is obtained by
+ // converting the constant’s IDL value to an ECMAScript
+ // value."
+ assert_equals(this.get_interface_object()[member.name], constValue(member.value),
+ "property has wrong value");
+ // "The property has attributes { [[Writable]]: false,
+ // [[Enumerable]]: true, [[Configurable]]: false }."
+ var desc = Object.getOwnPropertyDescriptor(this.get_interface_object(), member.name);
+ assert_false("get" in desc, "property should not have a getter");
+ assert_false("set" in desc, "property should not have a setter");
+ assert_false(desc.writable, "property should not be writable");
+ assert_true(desc.enumerable, "property should be enumerable");
+ assert_false(desc.configurable, "property should not be configurable");
+ }.bind(this), this.name + " interface: constant " + member.name + " on interface object");
+
+ // "In addition, a property with the same characteristics must
+ // exist on the interface prototype object."
+ subsetTestByKey(this.name, test, function()
+ {
+ this.assert_interface_object_exists();
+
+ if (this.is_callback()) {
+ assert_false("prototype" in this.get_interface_object(),
+ this.name + ' should not have a "prototype" property');
+ return;
+ }
+
+ assert_own_property(this.get_interface_object(), "prototype",
+ 'interface "' + this.name + '" does not have own property "prototype"');
+
+ assert_own_property(this.get_interface_object().prototype, member.name);
+ assert_equals(this.get_interface_object().prototype[member.name], constValue(member.value),
+ "property has wrong value");
+ var desc = Object.getOwnPropertyDescriptor(this.get_interface_object(), member.name);
+ assert_false("get" in desc, "property should not have a getter");
+ assert_false("set" in desc, "property should not have a setter");
+ assert_false(desc.writable, "property should not be writable");
+ assert_true(desc.enumerable, "property should be enumerable");
+ assert_false(desc.configurable, "property should not be configurable");
+ }.bind(this), this.name + " interface: constant " + member.name + " on interface prototype object");
+};
+
+
+IdlInterface.prototype.test_member_attribute = function(member)
+ {
+ if (!shouldRunSubTest(this.name)) {
+ return;
+ }
+ var a_test = subsetTestByKey(this.name, async_test, this.name + " interface: attribute " + member.name);
+ a_test.step(function()
+ {
+ if (!this.should_have_interface_object()) {
+ a_test.done();
+ return;
+ }
+
+ this.assert_interface_object_exists();
+ assert_own_property(this.get_interface_object(), "prototype",
+ 'interface "' + this.name + '" does not have own property "prototype"');
+
+ if (member.special === "static") {
+ assert_own_property(this.get_interface_object(), member.name,
+ "The interface object must have a property " +
+ format_value(member.name));
+ a_test.done();
+ return;
+ }
+
+ this.do_member_unscopable_asserts(member);
+
+ if (this.is_global()) {
+ assert_own_property(self, member.name,
+ "The global object must have a property " +
+ format_value(member.name));
+ assert_false(member.name in this.get_interface_object().prototype,
+ "The prototype object should not have a property " +
+ format_value(member.name));
+
+ var getter = Object.getOwnPropertyDescriptor(self, member.name).get;
+ assert_equals(typeof(getter), "function",
+ format_value(member.name) + " must have a getter");
+
+ // Try/catch around the get here, since it can legitimately throw.
+ // If it does, we obviously can't check for equality with direct
+ // invocation of the getter.
+ var gotValue;
+ var propVal;
+ try {
+ propVal = self[member.name];
+ gotValue = true;
+ } catch (e) {
+ gotValue = false;
+ }
+ if (gotValue) {
+ assert_equals(propVal, getter.call(undefined),
+ "Gets on a global should not require an explicit this");
+ }
+
+ // do_interface_attribute_asserts must be the last thing we do,
+ // since it will call done() on a_test.
+ this.do_interface_attribute_asserts(self, member, a_test);
+ } else {
+ assert_true(member.name in this.get_interface_object().prototype,
+ "The prototype object must have a property " +
+ format_value(member.name));
+
+ if (!member.has_extended_attribute("LegacyLenientThis")) {
+ if (member.idlType.generic !== "Promise") {
+ // this.get_interface_object() returns a thing in our global
+ assert_throws_js(TypeError, function() {
+ this.get_interface_object().prototype[member.name];
+ }.bind(this), "getting property on prototype object must throw TypeError");
+ // do_interface_attribute_asserts must be the last thing we
+ // do, since it will call done() on a_test.
+ this.do_interface_attribute_asserts(this.get_interface_object().prototype, member, a_test);
+ } else {
+ promise_rejects_js(a_test, TypeError,
+ this.get_interface_object().prototype[member.name])
+ .then(a_test.step_func(function() {
+ // do_interface_attribute_asserts must be the last
+ // thing we do, since it will call done() on a_test.
+ this.do_interface_attribute_asserts(this.get_interface_object().prototype,
+ member, a_test);
+ }.bind(this)));
+ }
+ } else {
+ assert_equals(this.get_interface_object().prototype[member.name], undefined,
+ "getting property on prototype object must return undefined");
+ // do_interface_attribute_asserts must be the last thing we do,
+ // since it will call done() on a_test.
+ this.do_interface_attribute_asserts(this.get_interface_object().prototype, member, a_test);
+ }
+ }
+ }.bind(this));
+};
+
+IdlInterface.prototype.test_member_operation = function(member)
+{
+ if (!shouldRunSubTest(this.name)) {
+ return;
+ }
+ var a_test = subsetTestByKey(this.name, async_test, this.name + " interface: operation " + member);
+ a_test.step(function()
+ {
+ // This function tests WebIDL as of 2015-12-29.
+ // https://webidl.spec.whatwg.org/#es-operations
+
+ if (!this.should_have_interface_object()) {
+ a_test.done();
+ return;
+ }
+
+ this.assert_interface_object_exists();
+
+ if (this.is_callback()) {
+ assert_false("prototype" in this.get_interface_object(),
+ this.name + ' should not have a "prototype" property');
+ a_test.done();
+ return;
+ }
+
+ assert_own_property(this.get_interface_object(), "prototype",
+ 'interface "' + this.name + '" does not have own property "prototype"');
+
+ // "For each unique identifier of an exposed operation defined on the
+ // interface, there must exist a corresponding property, unless the
+ // effective overload set for that identifier and operation and with an
+ // argument count of 0 has no entries."
+
+ // TODO: Consider [Exposed].
+
+ // "The location of the property is determined as follows:"
+ var memberHolderObject;
+ // "* If the operation is static, then the property exists on the
+ // interface object."
+ if (member.special === "static") {
+ assert_own_property(this.get_interface_object(), member.name,
+ "interface object missing static operation");
+ memberHolderObject = this.get_interface_object();
+ // "* Otherwise, [...] if the interface was declared with the [Global]
+ // extended attribute, then the property exists
+ // on every object that implements the interface."
+ } else if (this.is_global()) {
+ assert_own_property(self, member.name,
+ "global object missing non-static operation");
+ memberHolderObject = self;
+ // "* Otherwise, the property exists solely on the interface’s
+ // interface prototype object."
+ } else {
+ assert_own_property(this.get_interface_object().prototype, member.name,
+ "interface prototype object missing non-static operation");
+ memberHolderObject = this.get_interface_object().prototype;
+ }
+ this.do_member_unscopable_asserts(member);
+ this.do_member_operation_asserts(memberHolderObject, member, a_test);
+ }.bind(this));
+};
+
+IdlInterface.prototype.do_member_unscopable_asserts = function(member)
+{
+ // Check that if the member is unscopable then it's in the
+ // @@unscopables object properly.
+ if (!member.isUnscopable) {
+ return;
+ }
+
+ var unscopables = this.get_interface_object().prototype[Symbol.unscopables];
+ var prop = member.name;
+ var propDesc = Object.getOwnPropertyDescriptor(unscopables, prop);
+ assert_equals(typeof propDesc, "object",
+ this.name + '.prototype[Symbol.unscopables].' + prop + ' must exist')
+ assert_false("get" in propDesc,
+ this.name + '.prototype[Symbol.unscopables].' + prop + ' must have no getter');
+ assert_false("set" in propDesc,
+ this.name + '.prototype[Symbol.unscopables].' + prop + ' must have no setter');
+ assert_true(propDesc.writable,
+ this.name + '.prototype[Symbol.unscopables].' + prop + ' must be writable');
+ assert_true(propDesc.enumerable,
+ this.name + '.prototype[Symbol.unscopables].' + prop + ' must be enumerable');
+ assert_true(propDesc.configurable,
+ this.name + '.prototype[Symbol.unscopables].' + prop + ' must be configurable');
+ assert_equals(propDesc.value, true,
+ this.name + '.prototype[Symbol.unscopables].' + prop + ' must have the value `true`');
+};
+
+IdlInterface.prototype.do_member_operation_asserts = function(memberHolderObject, member, a_test)
+{
+ var done = a_test.done.bind(a_test);
+ var operationUnforgeable = member.isUnforgeable;
+ var desc = Object.getOwnPropertyDescriptor(memberHolderObject, member.name);
+ // "The property has attributes { [[Writable]]: B,
+ // [[Enumerable]]: true, [[Configurable]]: B }, where B is false if the
+ // operation is unforgeable on the interface, and true otherwise".
+ assert_false("get" in desc, "property should not have a getter");
+ assert_false("set" in desc, "property should not have a setter");
+ assert_equals(desc.writable, !operationUnforgeable,
+ "property should be writable if and only if not unforgeable");
+ assert_true(desc.enumerable, "property should be enumerable");
+ assert_equals(desc.configurable, !operationUnforgeable,
+ "property should be configurable if and only if not unforgeable");
+ // "The value of the property is a Function object whose
+ // behavior is as follows . . ."
+ assert_equals(typeof memberHolderObject[member.name], "function",
+ "property must be a function");
+
+ const ctors = this.members.filter(function(m) {
+ return m.type == "operation" && m.name == member.name;
+ });
+ assert_equals(
+ memberHolderObject[member.name].length,
+ minOverloadLength(ctors),
+ "property has wrong .length");
+ assert_equals(
+ memberHolderObject[member.name].name,
+ member.name,
+ "property has wrong .name");
+
+ // Make some suitable arguments
+ var args = member.arguments.map(function(arg) {
+ return create_suitable_object(arg.idlType);
+ });
+
+ // "Let O be a value determined as follows:
+ // ". . .
+ // "Otherwise, throw a TypeError."
+ // This should be hit if the operation is not static, there is
+ // no [ImplicitThis] attribute, and the this value is null.
+ //
+ // TODO: We currently ignore the [ImplicitThis] case. Except we manually
+ // check for globals, since otherwise we'll invoke window.close(). And we
+ // have to skip this test for anything that on the proto chain of "self",
+ // since that does in fact have implicit-this behavior.
+ if (member.special !== "static") {
+ var cb;
+ if (!this.is_global() &&
+ memberHolderObject[member.name] != self[member.name])
+ {
+ cb = awaitNCallbacks(2, done);
+ throwOrReject(a_test, member, memberHolderObject[member.name], null, args,
+ "calling operation with this = null didn't throw TypeError", cb);
+ } else {
+ cb = awaitNCallbacks(1, done);
+ }
+
+ // ". . . If O is not null and is also not a platform object
+ // that implements interface I, throw a TypeError."
+ //
+ // TODO: Test a platform object that implements some other
+ // interface. (Have to be sure to get inheritance right.)
+ throwOrReject(a_test, member, memberHolderObject[member.name], {}, args,
+ "calling operation with this = {} didn't throw TypeError", cb);
+ } else {
+ done();
+ }
+}
+
+IdlInterface.prototype.test_to_json_operation = function(desc, memberHolderObject, member) {
+ var instanceName = memberHolderObject && memberHolderObject.constructor.name
+ || member.name + " object";
+ if (member.has_extended_attribute("Default")) {
+ subsetTestByKey(this.name, test, function() {
+ var map = this.default_to_json_operation();
+ var json = memberHolderObject.toJSON();
+ map.forEach(function(type, k) {
+ assert_true(k in json, "property " + JSON.stringify(k) + " should be present in the output of " + this.name + ".prototype.toJSON()");
+ var descriptor = Object.getOwnPropertyDescriptor(json, k);
+ assert_true(descriptor.writable, "property " + k + " should be writable");
+ assert_true(descriptor.configurable, "property " + k + " should be configurable");
+ assert_true(descriptor.enumerable, "property " + k + " should be enumerable");
+ this.array.assert_type_is(json[k], type);
+ delete json[k];
+ }, this);
+ }.bind(this), this.name + " interface: default toJSON operation on " + desc);
+ } else {
+ subsetTestByKey(this.name, test, function() {
+ assert_true(this.array.is_json_type(member.idlType), JSON.stringify(member.idlType) + " is not an appropriate return value for the toJSON operation of " + instanceName);
+ this.array.assert_type_is(memberHolderObject.toJSON(), member.idlType);
+ }.bind(this), this.name + " interface: toJSON operation on " + desc);
+ }
+};
+
+IdlInterface.prototype.test_member_maplike = function(member) {
+ subsetTestByKey(this.name, test, () => {
+ const proto = this.get_interface_object().prototype;
+
+ const methods = [
+ ["entries", 0],
+ ["keys", 0],
+ ["values", 0],
+ ["forEach", 1],
+ ["get", 1],
+ ["has", 1]
+ ];
+ if (!member.readonly) {
+ methods.push(
+ ["set", 2],
+ ["delete", 1],
+ ["clear", 0]
+ );
+ }
+
+ for (const [name, length] of methods) {
+ const desc = Object.getOwnPropertyDescriptor(proto, name);
+ assert_equals(typeof desc.value, "function", `${name} should be a function`);
+ assert_equals(desc.enumerable, true, `${name} enumerable`);
+ assert_equals(desc.configurable, true, `${name} configurable`);
+ assert_equals(desc.writable, true, `${name} writable`);
+ assert_equals(desc.value.length, length, `${name} function object length should be ${length}`);
+ assert_equals(desc.value.name, name, `${name} function object should have the right name`);
+ }
+
+ const iteratorDesc = Object.getOwnPropertyDescriptor(proto, Symbol.iterator);
+ assert_equals(iteratorDesc.value, proto.entries, `@@iterator should equal entries`);
+ assert_equals(iteratorDesc.enumerable, false, `@@iterator enumerable`);
+ assert_equals(iteratorDesc.configurable, true, `@@iterator configurable`);
+ assert_equals(iteratorDesc.writable, true, `@@iterator writable`);
+
+ const sizeDesc = Object.getOwnPropertyDescriptor(proto, "size");
+ assert_equals(typeof sizeDesc.get, "function", `size getter should be a function`);
+ assert_equals(sizeDesc.set, undefined, `size should not have a setter`);
+ assert_equals(sizeDesc.enumerable, true, `size enumerable`);
+ assert_equals(sizeDesc.configurable, true, `size configurable`);
+ assert_equals(sizeDesc.get.length, 0, `size getter length`);
+ assert_equals(sizeDesc.get.name, "get size", `size getter name`);
+ }, `${this.name} interface: maplike<${member.idlType.map(t => t.idlType).join(", ")}>`);
+};
+
+IdlInterface.prototype.test_member_setlike = function(member) {
+ subsetTestByKey(this.name, test, () => {
+ const proto = this.get_interface_object().prototype;
+
+ const methods = [
+ ["entries", 0],
+ ["keys", 0],
+ ["values", 0],
+ ["forEach", 1],
+ ["has", 1]
+ ];
+ if (!member.readonly) {
+ methods.push(
+ ["add", 1],
+ ["delete", 1],
+ ["clear", 0]
+ );
+ }
+
+ for (const [name, length] of methods) {
+ const desc = Object.getOwnPropertyDescriptor(proto, name);
+ assert_equals(typeof desc.value, "function", `${name} should be a function`);
+ assert_equals(desc.enumerable, true, `${name} enumerable`);
+ assert_equals(desc.configurable, true, `${name} configurable`);
+ assert_equals(desc.writable, true, `${name} writable`);
+ assert_equals(desc.value.length, length, `${name} function object length should be ${length}`);
+ assert_equals(desc.value.name, name, `${name} function object should have the right name`);
+ }
+
+ const iteratorDesc = Object.getOwnPropertyDescriptor(proto, Symbol.iterator);
+ assert_equals(iteratorDesc.value, proto.values, `@@iterator should equal values`);
+ assert_equals(iteratorDesc.enumerable, false, `@@iterator enumerable`);
+ assert_equals(iteratorDesc.configurable, true, `@@iterator configurable`);
+ assert_equals(iteratorDesc.writable, true, `@@iterator writable`);
+
+ const sizeDesc = Object.getOwnPropertyDescriptor(proto, "size");
+ assert_equals(typeof sizeDesc.get, "function", `size getter should be a function`);
+ assert_equals(sizeDesc.set, undefined, `size should not have a setter`);
+ assert_equals(sizeDesc.enumerable, true, `size enumerable`);
+ assert_equals(sizeDesc.configurable, true, `size configurable`);
+ assert_equals(sizeDesc.get.length, 0, `size getter length`);
+ assert_equals(sizeDesc.get.name, "get size", `size getter name`);
+ }, `${this.name} interface: setlike<${member.idlType.map(t => t.idlType).join(", ")}>`);
+};
+
+IdlInterface.prototype.test_member_iterable = function(member) {
+ subsetTestByKey(this.name, test, () => {
+ const isPairIterator = member.idlType.length === 2;
+ const proto = this.get_interface_object().prototype;
+
+ const methods = [
+ ["entries", 0],
+ ["keys", 0],
+ ["values", 0],
+ ["forEach", 1]
+ ];
+
+ for (const [name, length] of methods) {
+ const desc = Object.getOwnPropertyDescriptor(proto, name);
+ assert_equals(typeof desc.value, "function", `${name} should be a function`);
+ assert_equals(desc.enumerable, true, `${name} enumerable`);
+ assert_equals(desc.configurable, true, `${name} configurable`);
+ assert_equals(desc.writable, true, `${name} writable`);
+ assert_equals(desc.value.length, length, `${name} function object length should be ${length}`);
+ assert_equals(desc.value.name, name, `${name} function object should have the right name`);
+
+ if (!isPairIterator) {
+ assert_equals(desc.value, Array.prototype[name], `${name} equality with Array.prototype version`);
+ }
+ }
+
+ const iteratorDesc = Object.getOwnPropertyDescriptor(proto, Symbol.iterator);
+ assert_equals(iteratorDesc.enumerable, false, `@@iterator enumerable`);
+ assert_equals(iteratorDesc.configurable, true, `@@iterator configurable`);
+ assert_equals(iteratorDesc.writable, true, `@@iterator writable`);
+
+ if (isPairIterator) {
+ assert_equals(iteratorDesc.value, proto.entries, `@@iterator equality with entries`);
+ } else {
+ assert_equals(iteratorDesc.value, Array.prototype[Symbol.iterator], `@@iterator equality with Array.prototype version`);
+ }
+ }, `${this.name} interface: iterable<${member.idlType.map(t => t.idlType).join(", ")}>`);
+};
+
+IdlInterface.prototype.test_member_async_iterable = function(member) {
+ subsetTestByKey(this.name, test, () => {
+ const isPairIterator = member.idlType.length === 2;
+ const proto = this.get_interface_object().prototype;
+
+ // Note that although the spec allows arguments, which will be passed to the @@asyncIterator
+ // method (which is either values or entries), those arguments must always be optional. So
+ // length of 0 is still correct for values and entries.
+ const methods = [
+ ["values", 0],
+ ];
+
+ if (isPairIterator) {
+ methods.push(
+ ["entries", 0],
+ ["keys", 0]
+ );
+ }
+
+ for (const [name, length] of methods) {
+ const desc = Object.getOwnPropertyDescriptor(proto, name);
+ assert_equals(typeof desc.value, "function", `${name} should be a function`);
+ assert_equals(desc.enumerable, true, `${name} enumerable`);
+ assert_equals(desc.configurable, true, `${name} configurable`);
+ assert_equals(desc.writable, true, `${name} writable`);
+ assert_equals(desc.value.length, length, `${name} function object length should be ${length}`);
+ assert_equals(desc.value.name, name, `${name} function object should have the right name`);
+ }
+
+ const iteratorDesc = Object.getOwnPropertyDescriptor(proto, Symbol.asyncIterator);
+ assert_equals(iteratorDesc.enumerable, false, `@@iterator enumerable`);
+ assert_equals(iteratorDesc.configurable, true, `@@iterator configurable`);
+ assert_equals(iteratorDesc.writable, true, `@@iterator writable`);
+
+ if (isPairIterator) {
+ assert_equals(iteratorDesc.value, proto.entries, `@@iterator equality with entries`);
+ } else {
+ assert_equals(iteratorDesc.value, proto.values, `@@iterator equality with values`);
+ }
+ }, `${this.name} interface: async iterable<${member.idlType.map(t => t.idlType).join(", ")}>`);
+};
+
+IdlInterface.prototype.test_member_stringifier = function(member)
+{
+ subsetTestByKey(this.name, test, function()
+ {
+ if (!this.should_have_interface_object()) {
+ return;
+ }
+
+ this.assert_interface_object_exists();
+
+ if (this.is_callback()) {
+ assert_false("prototype" in this.get_interface_object(),
+ this.name + ' should not have a "prototype" property');
+ return;
+ }
+
+ assert_own_property(this.get_interface_object(), "prototype",
+ 'interface "' + this.name + '" does not have own property "prototype"');
+
+ // ". . . the property exists on the interface prototype object."
+ var interfacePrototypeObject = this.get_interface_object().prototype;
+ assert_own_property(interfacePrototypeObject, "toString",
+ "interface prototype object missing non-static operation");
+
+ var stringifierUnforgeable = member.isUnforgeable;
+ var desc = Object.getOwnPropertyDescriptor(interfacePrototypeObject, "toString");
+ // "The property has attributes { [[Writable]]: B,
+ // [[Enumerable]]: true, [[Configurable]]: B }, where B is false if the
+ // stringifier is unforgeable on the interface, and true otherwise."
+ assert_false("get" in desc, "property should not have a getter");
+ assert_false("set" in desc, "property should not have a setter");
+ assert_equals(desc.writable, !stringifierUnforgeable,
+ "property should be writable if and only if not unforgeable");
+ assert_true(desc.enumerable, "property should be enumerable");
+ assert_equals(desc.configurable, !stringifierUnforgeable,
+ "property should be configurable if and only if not unforgeable");
+ // "The value of the property is a Function object, which behaves as
+ // follows . . ."
+ assert_equals(typeof interfacePrototypeObject.toString, "function",
+ "property must be a function");
+ // "The value of the Function object’s “length” property is the Number
+ // value 0."
+ assert_equals(interfacePrototypeObject.toString.length, 0,
+ "property has wrong .length");
+
+ // "Let O be the result of calling ToObject on the this value."
+ assert_throws_js(globalOf(interfacePrototypeObject.toString).TypeError, function() {
+ interfacePrototypeObject.toString.apply(null, []);
+ }, "calling stringifier with this = null didn't throw TypeError");
+
+ // "If O is not an object that implements the interface on which the
+ // stringifier was declared, then throw a TypeError."
+ //
+ // TODO: Test a platform object that implements some other
+ // interface. (Have to be sure to get inheritance right.)
+ assert_throws_js(globalOf(interfacePrototypeObject.toString).TypeError, function() {
+ interfacePrototypeObject.toString.apply({}, []);
+ }, "calling stringifier with this = {} didn't throw TypeError");
+ }.bind(this), this.name + " interface: stringifier");
+};
+
+IdlInterface.prototype.test_members = function()
+{
+ var unexposed_members = new Set();
+ for (var i = 0; i < this.members.length; i++)
+ {
+ var member = this.members[i];
+ if (member.untested) {
+ continue;
+ }
+
+ if (!exposed_in(exposure_set(member, this.exposureSet))) {
+ if (!unexposed_members.has(member.name)) {
+ unexposed_members.add(member.name);
+ subsetTestByKey(this.name, test, function() {
+ // It's not exposed, so we shouldn't find it anywhere.
+ assert_false(member.name in this.get_interface_object(),
+ "The interface object must not have a property " +
+ format_value(member.name));
+ assert_false(member.name in this.get_interface_object().prototype,
+ "The prototype object must not have a property " +
+ format_value(member.name));
+ }.bind(this), this.name + " interface: member " + member.name);
+ }
+ continue;
+ }
+
+ switch (member.type) {
+ case "const":
+ this.test_member_const(member);
+ break;
+
+ case "attribute":
+ // For unforgeable attributes, we do the checks in
+ // test_interface_of instead.
+ if (!member.isUnforgeable)
+ {
+ this.test_member_attribute(member);
+ }
+ if (member.special === "stringifier") {
+ this.test_member_stringifier(member);
+ }
+ break;
+
+ case "operation":
+ // TODO: Need to correctly handle multiple operations with the same
+ // identifier.
+ // For unforgeable operations, we do the checks in
+ // test_interface_of instead.
+ if (member.name) {
+ if (!member.isUnforgeable)
+ {
+ this.test_member_operation(member);
+ }
+ } else if (member.special === "stringifier") {
+ this.test_member_stringifier(member);
+ }
+ break;
+
+ case "iterable":
+ if (member.async) {
+ this.test_member_async_iterable(member);
+ } else {
+ this.test_member_iterable(member);
+ }
+ break;
+ case "maplike":
+ this.test_member_maplike(member);
+ break;
+ case "setlike":
+ this.test_member_setlike(member);
+ break;
+ default:
+ // TODO: check more member types.
+ break;
+ }
+ }
+};
+
+IdlInterface.prototype.test_object = function(desc)
+{
+ var obj, exception = null;
+ try
+ {
+ obj = eval(desc);
+ }
+ catch(e)
+ {
+ exception = e;
+ }
+
+ var expected_typeof;
+ if (this.name == "HTMLAllCollection")
+ {
+ // Result of [[IsHTMLDDA]] slot
+ expected_typeof = "undefined";
+ }
+ else
+ {
+ expected_typeof = "object";
+ }
+
+ if (this.is_callback()) {
+ assert_equals(exception, null, "Unexpected exception when evaluating object");
+ assert_equals(typeof obj, expected_typeof, "wrong typeof object");
+ } else {
+ this.test_primary_interface_of(desc, obj, exception, expected_typeof);
+
+ var current_interface = this;
+ while (current_interface)
+ {
+ if (!(current_interface.name in this.array.members))
+ {
+ throw new IdlHarnessError("Interface " + current_interface.name + " not found (inherited by " + this.name + ")");
+ }
+ if (current_interface.prevent_multiple_testing && current_interface.already_tested)
+ {
+ return;
+ }
+ current_interface.test_interface_of(desc, obj, exception, expected_typeof);
+ current_interface = this.array.members[current_interface.base];
+ }
+ }
+};
+
+IdlInterface.prototype.test_primary_interface_of = function(desc, obj, exception, expected_typeof)
+{
+ // Only the object itself, not its members, are tested here, so if the
+ // interface is untested, there is nothing to do.
+ if (this.untested)
+ {
+ return;
+ }
+
+ // "The internal [[SetPrototypeOf]] method of every platform object that
+ // implements an interface with the [Global] extended
+ // attribute must execute the same algorithm as is defined for the
+ // [[SetPrototypeOf]] internal method of an immutable prototype exotic
+ // object."
+ // https://webidl.spec.whatwg.org/#platform-object-setprototypeof
+ if (this.is_global())
+ {
+ this.test_immutable_prototype("global platform object", obj);
+ }
+
+
+ // We can't easily test that its prototype is correct if there's no
+ // interface object, or the object is from a different global environment
+ // (not instanceof Object). TODO: test in this case that its prototype at
+ // least looks correct, even if we can't test that it's actually correct.
+ if (this.should_have_interface_object()
+ && (typeof obj != expected_typeof || obj instanceof Object))
+ {
+ subsetTestByKey(this.name, test, function()
+ {
+ assert_equals(exception, null, "Unexpected exception when evaluating object");
+ assert_equals(typeof obj, expected_typeof, "wrong typeof object");
+ this.assert_interface_object_exists();
+ assert_own_property(this.get_interface_object(), "prototype",
+ 'interface "' + this.name + '" does not have own property "prototype"');
+
+ // "The value of the internal [[Prototype]] property of the
+ // platform object is the interface prototype object of the primary
+ // interface from the platform object’s associated global
+ // environment."
+ assert_equals(Object.getPrototypeOf(obj),
+ this.get_interface_object().prototype,
+ desc + "'s prototype is not " + this.name + ".prototype");
+ }.bind(this), this.name + " must be primary interface of " + desc);
+ }
+
+ // "The class string of a platform object that implements one or more
+ // interfaces must be the qualified name of the primary interface of the
+ // platform object."
+ subsetTestByKey(this.name, test, function()
+ {
+ assert_equals(exception, null, "Unexpected exception when evaluating object");
+ assert_equals(typeof obj, expected_typeof, "wrong typeof object");
+ assert_class_string(obj, this.get_qualified_name(), "class string of " + desc);
+ if (!this.has_stringifier())
+ {
+ assert_equals(String(obj), "[object " + this.get_qualified_name() + "]", "String(" + desc + ")");
+ }
+ }.bind(this), "Stringification of " + desc);
+};
+
+IdlInterface.prototype.test_interface_of = function(desc, obj, exception, expected_typeof)
+{
+ // TODO: Indexed and named properties, more checks on interface members
+ this.already_tested = true;
+ if (!shouldRunSubTest(this.name)) {
+ return;
+ }
+
+ var unexposed_properties = new Set();
+ for (var i = 0; i < this.members.length; i++)
+ {
+ var member = this.members[i];
+ if (member.untested) {
+ continue;
+ }
+ if (!exposed_in(exposure_set(member, this.exposureSet)))
+ {
+ if (!unexposed_properties.has(member.name))
+ {
+ unexposed_properties.add(member.name);
+ subsetTestByKey(this.name, test, function() {
+ assert_equals(exception, null, "Unexpected exception when evaluating object");
+ assert_false(member.name in obj);
+ }.bind(this), this.name + " interface: " + desc + ' must not have property "' + member.name + '"');
+ }
+ continue;
+ }
+ if (member.type == "attribute" && member.isUnforgeable)
+ {
+ var a_test = subsetTestByKey(this.name, async_test, this.name + " interface: " + desc + ' must have own property "' + member.name + '"');
+ a_test.step(function() {
+ assert_equals(exception, null, "Unexpected exception when evaluating object");
+ assert_equals(typeof obj, expected_typeof, "wrong typeof object");
+ // Call do_interface_attribute_asserts last, since it will call a_test.done()
+ this.do_interface_attribute_asserts(obj, member, a_test);
+ }.bind(this));
+ }
+ else if (member.type == "operation" &&
+ member.name &&
+ member.isUnforgeable)
+ {
+ var a_test = subsetTestByKey(this.name, async_test, this.name + " interface: " + desc + ' must have own property "' + member.name + '"');
+ a_test.step(function()
+ {
+ assert_equals(exception, null, "Unexpected exception when evaluating object");
+ assert_equals(typeof obj, expected_typeof, "wrong typeof object");
+ assert_own_property(obj, member.name,
+ "Doesn't have the unforgeable operation property");
+ this.do_member_operation_asserts(obj, member, a_test);
+ }.bind(this));
+ }
+ else if ((member.type == "const"
+ || member.type == "attribute"
+ || member.type == "operation")
+ && member.name)
+ {
+ subsetTestByKey(this.name, test, function()
+ {
+ assert_equals(exception, null, "Unexpected exception when evaluating object");
+ assert_equals(typeof obj, expected_typeof, "wrong typeof object");
+ if (member.special !== "static") {
+ if (!this.is_global()) {
+ assert_inherits(obj, member.name);
+ } else {
+ assert_own_property(obj, member.name);
+ }
+
+ if (member.type == "const")
+ {
+ assert_equals(obj[member.name], constValue(member.value));
+ }
+ if (member.type == "attribute")
+ {
+ // Attributes are accessor properties, so they might
+ // legitimately throw an exception rather than returning
+ // anything.
+ var property, thrown = false;
+ try
+ {
+ property = obj[member.name];
+ }
+ catch (e)
+ {
+ thrown = true;
+ }
+ if (!thrown)
+ {
+ if (this.name == "Document" && member.name == "all")
+ {
+ // Result of [[IsHTMLDDA]] slot
+ assert_equals(typeof property, "undefined");
+ }
+ else
+ {
+ this.array.assert_type_is(property, member.idlType);
+ }
+ }
+ }
+ if (member.type == "operation")
+ {
+ assert_equals(typeof obj[member.name], "function");
+ }
+ }
+ }.bind(this), this.name + " interface: " + desc + ' must inherit property "' + member + '" with the proper type');
+ }
+ // TODO: This is wrong if there are multiple operations with the same
+ // identifier.
+ // TODO: Test passing arguments of the wrong type.
+ if (member.type == "operation" && member.name && member.arguments.length)
+ {
+ var description =
+ this.name + " interface: calling " + member + " on " + desc +
+ " with too few arguments must throw TypeError";
+ var a_test = subsetTestByKey(this.name, async_test, description);
+ a_test.step(function()
+ {
+ assert_equals(exception, null, "Unexpected exception when evaluating object");
+ assert_equals(typeof obj, expected_typeof, "wrong typeof object");
+ var fn;
+ if (member.special !== "static") {
+ if (!this.is_global() && !member.isUnforgeable) {
+ assert_inherits(obj, member.name);
+ } else {
+ assert_own_property(obj, member.name);
+ }
+ fn = obj[member.name];
+ }
+ else
+ {
+ assert_own_property(obj.constructor, member.name, "interface object must have static operation as own property");
+ fn = obj.constructor[member.name];
+ }
+
+ var minLength = minOverloadLength(this.members.filter(function(m) {
+ return m.type == "operation" && m.name == member.name;
+ }));
+ var args = [];
+ var cb = awaitNCallbacks(minLength, a_test.done.bind(a_test));
+ for (var i = 0; i < minLength; i++) {
+ throwOrReject(a_test, member, fn, obj, args, "Called with " + i + " arguments", cb);
+
+ args.push(create_suitable_object(member.arguments[i].idlType));
+ }
+ if (minLength === 0) {
+ cb();
+ }
+ }.bind(this));
+ }
+
+ if (member.is_to_json_regular_operation()) {
+ this.test_to_json_operation(desc, obj, member);
+ }
+ }
+};
+
+IdlInterface.prototype.has_stringifier = function()
+{
+ if (this.name === "DOMException") {
+ // toString is inherited from Error, so don't assume we have the
+ // default stringifer
+ return true;
+ }
+ if (this.members.some(function(member) { return member.special === "stringifier"; })) {
+ return true;
+ }
+ if (this.base &&
+ this.array.members[this.base].has_stringifier()) {
+ return true;
+ }
+ return false;
+};
+
+IdlInterface.prototype.do_interface_attribute_asserts = function(obj, member, a_test)
+{
+ // This function tests WebIDL as of 2015-01-27.
+ // TODO: Consider [Exposed].
+
+ // This is called by test_member_attribute() with the prototype as obj if
+ // it is not a global, and the global otherwise, and by test_interface_of()
+ // with the object as obj.
+
+ var pendingPromises = [];
+
+ // "The name of the property is the identifier of the attribute."
+ assert_own_property(obj, member.name);
+
+ // "The property has attributes { [[Get]]: G, [[Set]]: S, [[Enumerable]]:
+ // true, [[Configurable]]: configurable }, where:
+ // "configurable is false if the attribute was declared with the
+ // [LegacyUnforgeable] extended attribute and true otherwise;
+ // "G is the attribute getter, defined below; and
+ // "S is the attribute setter, also defined below."
+ var desc = Object.getOwnPropertyDescriptor(obj, member.name);
+ assert_false("value" in desc, 'property descriptor should not have a "value" field');
+ assert_false("writable" in desc, 'property descriptor should not have a "writable" field');
+ assert_true(desc.enumerable, "property should be enumerable");
+ if (member.isUnforgeable)
+ {
+ assert_false(desc.configurable, "[LegacyUnforgeable] property must not be configurable");
+ }
+ else
+ {
+ assert_true(desc.configurable, "property must be configurable");
+ }
+
+
+ // "The attribute getter is a Function object whose behavior when invoked
+ // is as follows:"
+ assert_equals(typeof desc.get, "function", "getter must be Function");
+
+ // "If the attribute is a regular attribute, then:"
+ if (member.special !== "static") {
+ // "If O is not a platform object that implements I, then:
+ // "If the attribute was specified with the [LegacyLenientThis] extended
+ // attribute, then return undefined.
+ // "Otherwise, throw a TypeError."
+ if (!member.has_extended_attribute("LegacyLenientThis")) {
+ if (member.idlType.generic !== "Promise") {
+ assert_throws_js(globalOf(desc.get).TypeError, function() {
+ desc.get.call({});
+ }.bind(this), "calling getter on wrong object type must throw TypeError");
+ } else {
+ pendingPromises.push(
+ promise_rejects_js(a_test, TypeError, desc.get.call({}),
+ "calling getter on wrong object type must reject the return promise with TypeError"));
+ }
+ } else {
+ assert_equals(desc.get.call({}), undefined,
+ "calling getter on wrong object type must return undefined");
+ }
+ }
+
+ // "The value of the Function object’s “length” property is the Number
+ // value 0."
+ assert_equals(desc.get.length, 0, "getter length must be 0");
+
+ // "Let name be the string "get " prepended to attribute’s identifier."
+ // "Perform ! SetFunctionName(F, name)."
+ assert_equals(desc.get.name, "get " + member.name,
+ "getter must have the name 'get " + member.name + "'");
+
+
+ // TODO: Test calling setter on the interface prototype (should throw
+ // TypeError in most cases).
+ if (member.readonly
+ && !member.has_extended_attribute("LegacyLenientSetter")
+ && !member.has_extended_attribute("PutForwards")
+ && !member.has_extended_attribute("Replaceable"))
+ {
+ // "The attribute setter is undefined if the attribute is declared
+ // readonly and has neither a [PutForwards] nor a [Replaceable]
+ // extended attribute declared on it."
+ assert_equals(desc.set, undefined, "setter must be undefined for readonly attributes");
+ }
+ else
+ {
+ // "Otherwise, it is a Function object whose behavior when
+ // invoked is as follows:"
+ assert_equals(typeof desc.set, "function", "setter must be function for PutForwards, Replaceable, or non-readonly attributes");
+
+ // "If the attribute is a regular attribute, then:"
+ if (member.special !== "static") {
+ // "If /validThis/ is false and the attribute was not specified
+ // with the [LegacyLenientThis] extended attribute, then throw a
+ // TypeError."
+ // "If the attribute is declared with a [Replaceable] extended
+ // attribute, then: ..."
+ // "If validThis is false, then return."
+ if (!member.has_extended_attribute("LegacyLenientThis")) {
+ assert_throws_js(globalOf(desc.set).TypeError, function() {
+ desc.set.call({});
+ }.bind(this), "calling setter on wrong object type must throw TypeError");
+ } else {
+ assert_equals(desc.set.call({}), undefined,
+ "calling setter on wrong object type must return undefined");
+ }
+ }
+
+ // "The value of the Function object’s “length” property is the Number
+ // value 1."
+ assert_equals(desc.set.length, 1, "setter length must be 1");
+
+ // "Let name be the string "set " prepended to id."
+ // "Perform ! SetFunctionName(F, name)."
+ assert_equals(desc.set.name, "set " + member.name,
+ "The attribute setter must have the name 'set " + member.name + "'");
+ }
+
+ Promise.all(pendingPromises).then(a_test.done.bind(a_test));
+}
+
+/// IdlInterfaceMember ///
+function IdlInterfaceMember(obj)
+{
+ /**
+ * obj is an object produced by the WebIDLParser.js "ifMember" production.
+ * We just forward all properties to this object without modification,
+ * except for special extAttrs handling.
+ */
+ for (var k in obj.toJSON())
+ {
+ this[k] = obj[k];
+ }
+ if (!("extAttrs" in this))
+ {
+ this.extAttrs = [];
+ }
+
+ this.isUnforgeable = this.has_extended_attribute("LegacyUnforgeable");
+ this.isUnscopable = this.has_extended_attribute("Unscopable");
+}
+
+IdlInterfaceMember.prototype = Object.create(IdlObject.prototype);
+
+IdlInterfaceMember.prototype.toJSON = function() {
+ return this;
+};
+
+IdlInterfaceMember.prototype.is_to_json_regular_operation = function() {
+ return this.type == "operation" && this.special !== "static" && this.name == "toJSON";
+};
+
+IdlInterfaceMember.prototype.toString = function() {
+ function formatType(type) {
+ var result;
+ if (type.generic) {
+ result = type.generic + "<" + type.idlType.map(formatType).join(", ") + ">";
+ } else if (type.union) {
+ result = "(" + type.subtype.map(formatType).join(" or ") + ")";
+ } else {
+ result = type.idlType;
+ }
+ if (type.nullable) {
+ result += "?"
+ }
+ return result;
+ }
+
+ if (this.type === "operation") {
+ var args = this.arguments.map(function(m) {
+ return [
+ m.optional ? "optional " : "",
+ formatType(m.idlType),
+ m.variadic ? "..." : "",
+ ].join("");
+ }).join(", ");
+ return this.name + "(" + args + ")";
+ }
+
+ return this.name;
+}
+
+/// Internal helper functions ///
+function create_suitable_object(type)
+{
+ /**
+ * type is an object produced by the WebIDLParser.js "type" production. We
+ * return a JavaScript value that matches the type, if we can figure out
+ * how.
+ */
+ if (type.nullable)
+ {
+ return null;
+ }
+ switch (type.idlType)
+ {
+ case "any":
+ case "boolean":
+ return true;
+
+ case "byte": case "octet": case "short": case "unsigned short":
+ case "long": case "unsigned long": case "long long":
+ case "unsigned long long": case "float": case "double":
+ case "unrestricted float": case "unrestricted double":
+ return 7;
+
+ case "DOMString":
+ case "ByteString":
+ case "USVString":
+ return "foo";
+
+ case "object":
+ return {a: "b"};
+
+ case "Node":
+ return document.createTextNode("abc");
+ }
+ return null;
+}
+
+/// IdlEnum ///
+// Used for IdlArray.prototype.assert_type_is
+function IdlEnum(obj)
+{
+ /**
+ * obj is an object produced by the WebIDLParser.js "dictionary"
+ * production.
+ */
+
+ /** Self-explanatory. */
+ this.name = obj.name;
+
+ /** An array of values produced by the "enum" production. */
+ this.values = obj.values;
+
+}
+
+IdlEnum.prototype = Object.create(IdlObject.prototype);
+
+/// IdlCallback ///
+// Used for IdlArray.prototype.assert_type_is
+function IdlCallback(obj)
+{
+ /**
+ * obj is an object produced by the WebIDLParser.js "callback"
+ * production.
+ */
+
+ /** Self-explanatory. */
+ this.name = obj.name;
+
+ /** Arguments for the callback. */
+ this.arguments = obj.arguments;
+}
+
+IdlCallback.prototype = Object.create(IdlObject.prototype);
+
+/// IdlTypedef ///
+// Used for IdlArray.prototype.assert_type_is
+function IdlTypedef(obj)
+{
+ /**
+ * obj is an object produced by the WebIDLParser.js "typedef"
+ * production.
+ */
+
+ /** Self-explanatory. */
+ this.name = obj.name;
+
+ /** The idlType that we are supposed to be typedeffing to. */
+ this.idlType = obj.idlType;
+
+}
+
+IdlTypedef.prototype = Object.create(IdlObject.prototype);
+
+/// IdlNamespace ///
+function IdlNamespace(obj)
+{
+ this.name = obj.name;
+ this.extAttrs = obj.extAttrs;
+ this.untested = obj.untested;
+ /** A back-reference to our IdlArray. */
+ this.array = obj.array;
+
+ /** An array of IdlInterfaceMembers. */
+ this.members = obj.members.map(m => new IdlInterfaceMember(m));
+}
+
+IdlNamespace.prototype = Object.create(IdlObject.prototype);
+
+IdlNamespace.prototype.do_member_operation_asserts = function (memberHolderObject, member, a_test)
+{
+ var desc = Object.getOwnPropertyDescriptor(memberHolderObject, member.name);
+
+ assert_false("get" in desc, "property should not have a getter");
+ assert_false("set" in desc, "property should not have a setter");
+ assert_equals(
+ desc.writable,
+ !member.isUnforgeable,
+ "property should be writable if and only if not unforgeable");
+ assert_true(desc.enumerable, "property should be enumerable");
+ assert_equals(
+ desc.configurable,
+ !member.isUnforgeable,
+ "property should be configurable if and only if not unforgeable");
+
+ assert_equals(
+ typeof memberHolderObject[member.name],
+ "function",
+ "property must be a function");
+
+ assert_equals(
+ memberHolderObject[member.name].length,
+ minOverloadLength(this.members.filter(function(m) {
+ return m.type == "operation" && m.name == member.name;
+ })),
+ "operation has wrong .length");
+ a_test.done();
+}
+
+IdlNamespace.prototype.test_member_operation = function(member)
+{
+ if (!shouldRunSubTest(this.name)) {
+ return;
+ }
+ var a_test = subsetTestByKey(
+ this.name,
+ async_test,
+ this.name + ' namespace: operation ' + member);
+ a_test.step(function() {
+ assert_own_property(
+ self[this.name],
+ member.name,
+ 'namespace object missing operation ' + format_value(member.name));
+
+ this.do_member_operation_asserts(self[this.name], member, a_test);
+ }.bind(this));
+};
+
+IdlNamespace.prototype.test_member_attribute = function (member)
+{
+ if (!shouldRunSubTest(this.name)) {
+ return;
+ }
+ var a_test = subsetTestByKey(
+ this.name,
+ async_test,
+ this.name + ' namespace: attribute ' + member.name);
+ a_test.step(function()
+ {
+ assert_own_property(
+ self[this.name],
+ member.name,
+ this.name + ' does not have property ' + format_value(member.name));
+
+ var desc = Object.getOwnPropertyDescriptor(self[this.name], member.name);
+ assert_equals(desc.set, undefined, "setter must be undefined for namespace members");
+ a_test.done();
+ }.bind(this));
+};
+
+IdlNamespace.prototype.test_self = function ()
+{
+ /**
+ * TODO(lukebjerring): Assert:
+ * - "Note that unlike interfaces or dictionaries, namespaces do not create types."
+ */
+
+ subsetTestByKey(this.name, test, () => {
+ assert_true(this.extAttrs.every(o => o.name === "Exposed" || o.name === "SecureContext"),
+ "Only the [Exposed] and [SecureContext] extended attributes are applicable to namespaces");
+ assert_true(this.has_extended_attribute("Exposed"),
+ "Namespaces must be annotated with the [Exposed] extended attribute");
+ }, `${this.name} namespace: extended attributes`);
+
+ const namespaceObject = self[this.name];
+
+ subsetTestByKey(this.name, test, () => {
+ const desc = Object.getOwnPropertyDescriptor(self, this.name);
+ assert_equals(desc.value, namespaceObject, `wrong value for ${this.name} namespace object`);
+ assert_true(desc.writable, "namespace object should be writable");
+ assert_false(desc.enumerable, "namespace object should not be enumerable");
+ assert_true(desc.configurable, "namespace object should be configurable");
+ assert_false("get" in desc, "namespace object should not have a getter");
+ assert_false("set" in desc, "namespace object should not have a setter");
+ }, `${this.name} namespace: property descriptor`);
+
+ subsetTestByKey(this.name, test, () => {
+ assert_true(Object.isExtensible(namespaceObject));
+ }, `${this.name} namespace: [[Extensible]] is true`);
+
+ subsetTestByKey(this.name, test, () => {
+ assert_true(namespaceObject instanceof Object);
+
+ if (this.name === "console") {
+ // https://console.spec.whatwg.org/#console-namespace
+ const namespacePrototype = Object.getPrototypeOf(namespaceObject);
+ assert_equals(Reflect.ownKeys(namespacePrototype).length, 0);
+ assert_equals(Object.getPrototypeOf(namespacePrototype), Object.prototype);
+ } else {
+ assert_equals(Object.getPrototypeOf(namespaceObject), Object.prototype);
+ }
+ }, `${this.name} namespace: [[Prototype]] is Object.prototype`);
+
+ subsetTestByKey(this.name, test, () => {
+ assert_equals(typeof namespaceObject, "object");
+ }, `${this.name} namespace: typeof is "object"`);
+
+ subsetTestByKey(this.name, test, () => {
+ assert_equals(
+ Object.getOwnPropertyDescriptor(namespaceObject, "length"),
+ undefined,
+ "length property must be undefined"
+ );
+ }, `${this.name} namespace: has no length property`);
+
+ subsetTestByKey(this.name, test, () => {
+ assert_equals(
+ Object.getOwnPropertyDescriptor(namespaceObject, "name"),
+ undefined,
+ "name property must be undefined"
+ );
+ }, `${this.name} namespace: has no name property`);
+};
+
+IdlNamespace.prototype.test = function ()
+{
+ if (!this.untested) {
+ this.test_self();
+ }
+
+ for (const v of Object.values(this.members)) {
+ switch (v.type) {
+
+ case 'operation':
+ this.test_member_operation(v);
+ break;
+
+ case 'attribute':
+ this.test_member_attribute(v);
+ break;
+
+ default:
+ throw 'Invalid namespace member ' + v.name + ': ' + v.type + ' not supported';
+ }
+ };
+};
+
+}());
+
+/**
+ * idl_test is a promise_test wrapper that handles the fetching of the IDL,
+ * avoiding repetitive boilerplate.
+ *
+ * @param {String[]} srcs Spec name(s) for source idl files (fetched from
+ * /interfaces/{name}.idl).
+ * @param {String[]} deps Spec name(s) for dependency idl files (fetched
+ * from /interfaces/{name}.idl). Order is important - dependencies from
+ * each source will only be included if they're already know to be a
+ * dependency (i.e. have already been seen).
+ * @param {Function} setup_func Function for extra setup of the idl_array, such
+ * as adding objects. Do not call idl_array.test() in the setup; it is
+ * called by this function (idl_test).
+ */
+function idl_test(srcs, deps, idl_setup_func) {
+ return promise_test(function (t) {
+ var idl_array = new IdlArray();
+ var setup_error = null;
+ const validationIgnored = [
+ "constructor-member",
+ "dict-arg-default",
+ "require-exposed"
+ ];
+ return Promise.all(
+ srcs.concat(deps).map(fetch_spec))
+ .then(function(results) {
+ const astArray = results.map(result =>
+ WebIDL2.parse(result.idl, { sourceName: result.spec })
+ );
+ test(() => {
+ const validations = WebIDL2.validate(astArray)
+ .filter(v => !validationIgnored.includes(v.ruleName));
+ if (validations.length) {
+ const message = validations.map(v => v.message).join("\n\n");
+ throw new Error(message);
+ }
+ }, "idl_test validation");
+ for (var i = 0; i < srcs.length; i++) {
+ idl_array.internal_add_idls(astArray[i]);
+ }
+ for (var i = srcs.length; i < srcs.length + deps.length; i++) {
+ idl_array.internal_add_dependency_idls(astArray[i]);
+ }
+ })
+ .then(function() {
+ if (idl_setup_func) {
+ return idl_setup_func(idl_array, t);
+ }
+ })
+ .catch(function(e) { setup_error = e || 'IDL setup failed.'; })
+ .then(function () {
+ var error = setup_error;
+ try {
+ idl_array.test(); // Test what we can.
+ } catch (e) {
+ // If testing fails hard here, the original setup error
+ // is more likely to be the real cause.
+ error = error || e;
+ }
+ if (error) {
+ throw error;
+ }
+ });
+ }, 'idl_test setup');
+}
+
+/**
+ * fetch_spec is a shorthand for a Promise that fetches the spec's content.
+ */
+function fetch_spec(spec) {
+ var url = '/interfaces/' + spec + '.idl';
+ return fetch(url).then(function (r) {
+ if (!r.ok) {
+ throw new IdlHarnessError("Error fetching " + url + ".");
+ }
+ return r.text();
+ }).then(idl => ({ spec, idl }));
+}
+// vim: set expandtab shiftwidth=4 tabstop=4 foldmarker=@{,@} foldmethod=marker:
diff --git a/testing/web-platform/tests/resources/idlharness.js.headers b/testing/web-platform/tests/resources/idlharness.js.headers
new file mode 100644
index 0000000000..5e8f640c66
--- /dev/null
+++ b/testing/web-platform/tests/resources/idlharness.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/readme.md b/testing/web-platform/tests/resources/readme.md
new file mode 100644
index 0000000000..09a62fbd7b
--- /dev/null
+++ b/testing/web-platform/tests/resources/readme.md
@@ -0,0 +1,14 @@
+# Resources
+
+This directory contains utilities intended for use by tests and maintained as project infrastructure.
+It does not contain tests.
+
+## `testharness.js`
+
+`testharness.js` is a framework for writing low-level tests of
+browser functionality in javascript. It provides a convenient API for
+making assertions and is intended to work for both simple synchronous
+tests, and tests of asynchronous behaviour.
+
+Complete documentation is available in the `docs/` directory of this repository
+and on the web at https://web-platform-tests.org/writing-tests/.
diff --git a/testing/web-platform/tests/resources/sriharness.js b/testing/web-platform/tests/resources/sriharness.js
new file mode 100644
index 0000000000..943d677224
--- /dev/null
+++ b/testing/web-platform/tests/resources/sriharness.js
@@ -0,0 +1,226 @@
+// `integrityValue` indicates the 'integrity' attribute value at the time of
+// #prepare-a-script.
+//
+// `integrityValueAfterPrepare` indicates how the 'integrity' attribute value
+// is modified after #prepare-a-script:
+// - `undefined` => not modified.
+// - `null` => 'integrity' attribute is removed.
+// - others => 'integrity' attribute value is set to that value.
+//
+// TODO: Make the arguments a dictionary for readability in the test files.
+var SRIScriptTest = function(pass, name, src, integrityValue, crossoriginValue, nonce, integrityValueAfterPrepare) {
+ this.pass = pass;
+ this.name = "Script: " + name;
+ this.src = src;
+ this.integrityValue = integrityValue;
+ this.crossoriginValue = crossoriginValue;
+ this.nonce = nonce;
+ this.integrityValueAfterPrepare = integrityValueAfterPrepare;
+}
+
+SRIScriptTest.prototype.execute = function() {
+ var test = async_test(this.name);
+ var e = document.createElement("script");
+ e.src = this.src;
+ if (this.integrityValue) {
+ e.setAttribute("integrity", this.integrityValue);
+ }
+ if(this.crossoriginValue) {
+ e.setAttribute("crossorigin", this.crossoriginValue);
+ }
+ if(this.nonce) {
+ e.setAttribute("nonce", this.nonce);
+ }
+ if(this.pass) {
+ e.addEventListener("load", function() {test.done()});
+ e.addEventListener("error", function() {
+ test.step(function(){ assert_unreached("Good load fired error handler.") })
+ });
+ } else {
+ e.addEventListener("load", function() {
+ test.step(function() { assert_unreached("Bad load succeeded.") })
+ });
+ e.addEventListener("error", function() {test.done()});
+ }
+ document.body.appendChild(e);
+
+ if (this.integrityValueAfterPrepare === null) {
+ e.removeAttribute("integrity");
+ } else if (this.integrityValueAfterPrepare !== undefined) {
+ e.setAttribute("integrity", this.integrityValueAfterPrepare);
+ }
+};
+
+function set_extra_attributes(element, attrs) {
+ // Apply the rest of the attributes, if any.
+ for (const [attr_name, attr_val] of Object.entries(attrs)) {
+ element[attr_name] = attr_val;
+ }
+}
+
+function buildElementFromDestination(resource_url, destination, attrs) {
+ // Assert: |destination| is a valid destination.
+ let element;
+
+ // The below switch is responsible for:
+ // 1. Creating the correct subresource element
+ // 2. Setting said element's href, src, or fetch-instigating property
+ // appropriately.
+ switch (destination) {
+ case "script":
+ element = document.createElement(destination);
+ set_extra_attributes(element, attrs);
+ element.src = resource_url;
+ break;
+ case "style":
+ element = document.createElement('link');
+ set_extra_attributes(element, attrs);
+ element.rel = 'stylesheet';
+ element.href = resource_url;
+ break;
+ case "image":
+ element = document.createElement('img');
+ set_extra_attributes(element, attrs);
+ element.src = resource_url;
+ break;
+ default:
+ assert_unreached("INVALID DESTINATION");
+ }
+
+ return element;
+}
+
+// When using SRIPreloadTest, also include /preload/resources/preload_helper.js
+// |number_of_requests| is used to ensure that preload requests are actually
+// reused as expected.
+const SRIPreloadTest = (preload_sri_success, subresource_sri_success, name,
+ number_of_requests, destination, resource_url,
+ link_attrs, subresource_attrs) => {
+ const test = async_test(name);
+ const link = document.createElement('link');
+
+ // Early-fail in UAs that do not support `preload` links.
+ test.step_func(() => {
+ assert_true(link.relList.supports('preload'),
+ "This test is automatically failing because the browser does not" +
+ "support `preload` links.");
+ })();
+
+ // Build up the link.
+ link.rel = 'preload';
+ link.as = destination;
+ link.href = resource_url;
+ for (const [attr_name, attr_val] of Object.entries(link_attrs)) {
+ link[attr_name] = attr_val; // This may override `rel` to modulepreload.
+ }
+
+ // Preload + subresource success and failure loading functions.
+ const valid_preload_failed = test.step_func(() =>
+ { assert_unreached("Valid preload fired error handler.") });
+ const invalid_preload_succeeded = test.step_func(() =>
+ { assert_unreached("Invalid preload load succeeded.") });
+ const valid_subresource_failed = test.step_func(() =>
+ { assert_unreached("Valid subresource fired error handler.") });
+ const invalid_subresource_succeeded = test.step_func(() =>
+ { assert_unreached("Invalid subresource load succeeded.") });
+ const subresource_pass = test.step_func(() => {
+ verifyNumberOfResourceTimingEntries(resource_url, number_of_requests);
+ test.done();
+ });
+ const preload_pass = test.step_func(() => {
+ const subresource_element = buildElementFromDestination(
+ resource_url,
+ destination,
+ subresource_attrs
+ );
+
+ if (subresource_sri_success) {
+ subresource_element.onload = subresource_pass;
+ subresource_element.onerror = valid_subresource_failed;
+ } else {
+ subresource_element.onload = invalid_subresource_succeeded;
+ subresource_element.onerror = subresource_pass;
+ }
+
+ document.body.append(subresource_element);
+ });
+
+ if (preload_sri_success) {
+ link.onload = preload_pass;
+ link.onerror = valid_preload_failed;
+ } else {
+ link.onload = invalid_preload_succeeded;
+ link.onerror = preload_pass;
+ }
+
+ document.head.append(link);
+}
+
+// <link> tests
+// Style tests must be done synchronously because they rely on the presence
+// and absence of global style, which can affect later tests. Thus, instead
+// of executing them one at a time, the style tests are implemented as a
+// queue that builds up a list of tests, and then executes them one at a
+// time.
+var SRIStyleTest = function(queue, pass, name, attrs, customCallback, altPassValue) {
+ this.pass = pass;
+ this.name = "Style: " + name;
+ this.customCallback = customCallback || function () {};
+ this.attrs = attrs || {};
+ this.passValue = altPassValue || "rgb(255, 255, 0)";
+
+ this.test = async_test(this.name);
+
+ this.queue = queue;
+ this.queue.push(this);
+}
+
+SRIStyleTest.prototype.execute = function() {
+ var that = this;
+ var container = document.getElementById("container");
+ while (container.hasChildNodes()) {
+ container.removeChild(container.firstChild);
+ }
+
+ var test = this.test;
+
+ var div = document.createElement("div");
+ div.className = "testdiv";
+ var e = document.createElement("link");
+
+ // The link relation is guaranteed to not be "preload" or "modulepreload".
+ this.attrs.rel = this.attrs.rel || "stylesheet";
+ for (var key in this.attrs) {
+ if (this.attrs.hasOwnProperty(key)) {
+ e.setAttribute(key, this.attrs[key]);
+ }
+ }
+
+ if(this.pass) {
+ e.addEventListener("load", function() {
+ test.step(function() {
+ var background = window.getComputedStyle(div, null).getPropertyValue("background-color");
+ assert_equals(background, that.passValue);
+ test.done();
+ });
+ });
+ e.addEventListener("error", function() {
+ test.step(function(){ assert_unreached("Good load fired error handler.") })
+ });
+ } else {
+ e.addEventListener("load", function() {
+ test.step(function() { assert_unreached("Bad load succeeded.") })
+ });
+ e.addEventListener("error", function() {
+ test.step(function() {
+ var background = window.getComputedStyle(div, null).getPropertyValue("background-color");
+ assert_not_equals(background, that.passValue);
+ test.done();
+ });
+ });
+ }
+ container.appendChild(div);
+ container.appendChild(e);
+ this.customCallback(e, container);
+};
+
diff --git a/testing/web-platform/tests/resources/test-only-api.js b/testing/web-platform/tests/resources/test-only-api.js
new file mode 100644
index 0000000000..a66eb44ede
--- /dev/null
+++ b/testing/web-platform/tests/resources/test-only-api.js
@@ -0,0 +1,31 @@
+'use strict';
+
+/* Whether the browser is Chromium-based with MojoJS enabled */
+const isChromiumBased = 'MojoInterfaceInterceptor' in self;
+/* Whether the browser is WebKit-based with internal test-only API enabled */
+const isWebKitBased = !isChromiumBased && 'internals' in self;
+
+/**
+ * Loads a script in a window or worker.
+ *
+ * @param {string} path - A script path
+ * @returns {Promise}
+ */
+function loadScript(path) {
+ if (typeof document === 'undefined') {
+ // Workers (importScripts is synchronous and may throw.)
+ importScripts(path);
+ return Promise.resolve();
+ } else {
+ // Window
+ const script = document.createElement('script');
+ script.src = path;
+ script.async = false;
+ const p = new Promise((resolve, reject) => {
+ script.onload = () => { resolve(); };
+ script.onerror = e => { reject(`Error loading ${path}`); };
+ })
+ document.head.appendChild(script);
+ return p;
+ }
+}
diff --git a/testing/web-platform/tests/resources/test-only-api.js.headers b/testing/web-platform/tests/resources/test-only-api.js.headers
new file mode 100644
index 0000000000..5e8f640c66
--- /dev/null
+++ b/testing/web-platform/tests/resources/test-only-api.js.headers
@@ -0,0 +1,2 @@
+Content-Type: text/javascript; charset=utf-8
+Cache-Control: max-age=3600
diff --git a/testing/web-platform/tests/resources/test-only-api.m.js b/testing/web-platform/tests/resources/test-only-api.m.js
new file mode 100644
index 0000000000..984f635aba
--- /dev/null
+++ b/testing/web-platform/tests/resources/test-only-api.m.js
@@ -0,0 +1,5 @@
+/* Whether the browser is Chromium-based with MojoJS enabled */
+export const isChromiumBased = 'MojoInterfaceInterceptor' in self;
+
+/* Whether the browser is WebKit-based with internal test-only API enabled */
+export const isWebKitBased = !isChromiumBased && 'internals' in self;
diff --git a/testing/web-platform/tests/resources/test-only-api.m.js.headers b/testing/web-platform/tests/resources/test-only-api.m.js.headers
new file mode 100644
index 0000000000..5e8f640c66
--- /dev/null
+++ b/testing/web-platform/tests/resources/test-only-api.m.js.headers
@@ -0,0 +1,2 @@
+Content-Type: text/javascript; charset=utf-8
+Cache-Control: max-age=3600
diff --git a/testing/web-platform/tests/resources/test/README.md b/testing/web-platform/tests/resources/test/README.md
new file mode 100644
index 0000000000..edc03ef214
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/README.md
@@ -0,0 +1,83 @@
+# `testharness.js` test suite
+
+The test suite for the `testharness.js` testing framework.
+
+## Executing Tests
+
+Install the following dependencies:
+
+- [Python 2.7.9+](https://www.python.org/)
+- [the tox Python package](https://tox.readthedocs.io/en/latest/)
+- [the Mozilla Firefox web browser](https://mozilla.org/firefox)
+- [the GeckoDriver server](https://github.com/mozilla/geckodriver)
+
+Make sure `geckodriver` can be found in your `PATH`.
+
+Currently, the tests should be run with the latest *Firefox Nightly*. In order to
+specify the path to Firefox Nightly, use the following command-line option:
+
+ tox -- --binary=/path/to/FirefoxNightly
+
+### Automated Script
+
+Alternatively, you may run `tools/ci/ci_resources_unittest.sh`, which only depends on
+Python 2. The script will install other dependencies automatically and start `tox` with
+the correct arguments.
+
+## Authoring Tests
+
+Test cases are expressed as `.html` files located within the `tests/unit/` and
+`tests/functional/` sub-directories. Each test should include the
+`testharness.js` library with the following markup:
+
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+
+This should be followed by one or more `<script>` tags that interface with the
+`testharness.js` API in some way. For example:
+
+ <script>
+ test(function() {
+ 1 = 1;
+ }, 'This test is expected to fail.');
+ </script>
+
+### Unit tests
+
+The "unit test" type allows for concisely testing the expected behavior of
+assertion methods. These tests may define any number of sub-tests; the
+acceptance criteria is simply that all tests executed pass.
+
+### Functional tests
+
+Thoroughly testing the behavior of the harness itself requires ensuring a
+number of considerations which cannot be verified with the "unit testing"
+strategy. These include:
+
+- Ensuring that some tests are not run
+- Ensuring conditions that cause test failures
+- Ensuring conditions that cause harness errors
+
+Functional tests allow for these details to be verified. Every functional test
+must include a summary of the expected results as a JSON string within a
+`<script>` tag with an `id` of `"expected"`, e.g.:
+
+ <script type="text/json" id="expected">
+ {
+ "summarized_status": {
+ "message": null,
+ "stack": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "message": "ReferenceError: invalid assignment left-hand side",
+ "name": "Sample HTML5 API Tests",
+ "properties": {},
+ "stack": "(implementation-defined)",
+ "status_string": "FAIL"
+ }
+ ],
+ "type": "complete"
+ }
+ </script>
diff --git a/testing/web-platform/tests/resources/test/conftest.py b/testing/web-platform/tests/resources/test/conftest.py
new file mode 100644
index 0000000000..7253cac9ac
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/conftest.py
@@ -0,0 +1,269 @@
+import copy
+import json
+import os
+import ssl
+import sys
+import subprocess
+import urllib
+
+import html5lib
+import py
+import pytest
+
+from wptserver import WPTServer
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+WPT_ROOT = os.path.normpath(os.path.join(HERE, '..', '..'))
+HARNESS = os.path.join(HERE, 'harness.html')
+TEST_TYPES = ('functional', 'unit')
+
+sys.path.insert(0, os.path.normpath(os.path.join(WPT_ROOT, "tools")))
+import localpaths
+
+sys.path.insert(0, os.path.normpath(os.path.join(WPT_ROOT, "tools", "webdriver")))
+import webdriver
+
+
+def pytest_addoption(parser):
+ parser.addoption("--binary", action="store", default=None, help="path to browser binary")
+ parser.addoption("--headless", action="store_true", default=False, help="run browser in headless mode")
+
+
+def pytest_collect_file(file_path, path, parent):
+ if file_path.suffix.lower() != '.html':
+ return
+
+ # Tests are organized in directories by type
+ test_type = os.path.relpath(str(file_path), HERE)
+ if os.path.sep not in test_type or ".." in test_type:
+ # HTML files in this directory are not tests
+ return
+ test_type = test_type.split(os.path.sep)[1]
+
+ # Handle the deprecation of Node construction in pytest6
+ # https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent
+ if hasattr(HTMLItem, "from_parent"):
+ return HTMLItem.from_parent(parent, filename=str(file_path), test_type=test_type)
+ return HTMLItem(parent, str(file_path), test_type)
+
+
+def pytest_configure(config):
+ config.proc = subprocess.Popen(["geckodriver"])
+ config.add_cleanup(config.proc.kill)
+
+ capabilities = {"alwaysMatch": {"acceptInsecureCerts": True, "moz:firefoxOptions": {}}}
+ if config.getoption("--binary"):
+ capabilities["alwaysMatch"]["moz:firefoxOptions"]["binary"] = config.getoption("--binary")
+ if config.getoption("--headless"):
+ capabilities["alwaysMatch"]["moz:firefoxOptions"]["args"] = ["--headless"]
+
+ config.driver = webdriver.Session("localhost", 4444,
+ capabilities=capabilities)
+ config.add_cleanup(config.driver.end)
+
+ # Although the name of the `_create_unverified_context` method suggests
+ # that it is not intended for external consumption, the standard library's
+ # documentation explicitly endorses its use:
+ #
+ # > To revert to the previous, unverified, behavior
+ # > ssl._create_unverified_context() can be passed to the context
+ # > parameter.
+ #
+ # https://docs.python.org/2/library/httplib.html#httplib.HTTPSConnection
+ config.ssl_context = ssl._create_unverified_context()
+
+ config.server = WPTServer(WPT_ROOT)
+ config.server.start(config.ssl_context)
+ config.add_cleanup(config.server.stop)
+
+
+def resolve_uri(context, uri):
+ if uri.startswith('/'):
+ base = WPT_ROOT
+ path = uri[1:]
+ else:
+ base = os.path.dirname(context)
+ path = uri
+
+ return os.path.exists(os.path.join(base, path))
+
+
+def _summarize(actual):
+ def _scrub_stack(test_obj):
+ copy = dict(test_obj)
+ del copy['stack']
+ return copy
+
+ def _expand_status(status_obj):
+ for key, value in [item for item in status_obj.items()]:
+ # In "status" and "test" objects, the "status" value enum
+ # definitions are interspersed with properties for unrelated
+ # metadata. The following condition is a best-effort attempt to
+ # ignore non-enum properties.
+ if key != key.upper() or not isinstance(value, int):
+ continue
+
+ del status_obj[key]
+
+ if status_obj['status'] == value:
+ status_obj[u'status_string'] = key
+
+ del status_obj['status']
+
+ return status_obj
+
+ def _summarize_test(test_obj):
+ del test_obj['index']
+
+ assert 'phase' in test_obj
+ assert 'phases' in test_obj
+ assert 'COMPLETE' in test_obj['phases']
+ assert test_obj['phase'] == test_obj['phases']['COMPLETE']
+ del test_obj['phases']
+ del test_obj['phase']
+
+ return _expand_status(_scrub_stack(test_obj))
+
+ def _summarize_status(status_obj):
+ return _expand_status(_scrub_stack(status_obj))
+
+
+ summarized = {}
+
+ summarized[u'summarized_status'] = _summarize_status(actual['status'])
+ summarized[u'summarized_tests'] = [
+ _summarize_test(test) for test in actual['tests']]
+ summarized[u'summarized_tests'].sort(key=lambda test_obj: test_obj.get('name'))
+ summarized[u'summarized_asserts'] = [
+ {"assert_name": assert_item["assert_name"],
+ "test": assert_item["test"]["name"] if assert_item["test"] else None,
+ "args": assert_item["args"],
+ "status": assert_item["status"]} for assert_item in actual["asserts"]]
+ summarized[u'type'] = actual['type']
+
+ return summarized
+
+
+class HTMLItem(pytest.Item, pytest.Collector):
+ def __init__(self, parent, filename, test_type):
+ self.url = parent.session.config.server.url(filename)
+ self.type = test_type
+ # Some tests are reliant on the WPT servers substitution functionality,
+ # so tests must be retrieved from the server rather than read from the
+ # file system directly.
+ handle = urllib.request.urlopen(self.url,
+ context=parent.session.config.ssl_context)
+ try:
+ markup = handle.read()
+ finally:
+ handle.close()
+
+ if test_type not in TEST_TYPES:
+ raise ValueError('Unrecognized test type: "%s"' % test_type)
+
+ parsed = html5lib.parse(markup, namespaceHTMLElements=False)
+ name = None
+ self.expected = None
+
+ for element in parsed.iter():
+ if not name and element.tag == 'title':
+ name = element.text
+ continue
+ if element.tag == 'script':
+ if element.attrib.get('id') == 'expected':
+ try:
+ self.expected = json.loads(element.text)
+ except ValueError:
+ print("Failed parsing JSON in %s" % filename)
+ raise
+
+ if not name:
+ raise ValueError('No name found in %s add a <title> element' % filename)
+ elif self.type == 'functional':
+ if not self.expected:
+ raise ValueError('Functional tests must specify expected report data')
+ elif self.type == 'unit' and self.expected:
+ raise ValueError('Unit tests must not specify expected report data')
+
+ # Ensure that distinct items have distinct fspath attributes.
+ # This is necessary because pytest has an internal cache keyed on it,
+ # and only the first test with any given fspath will be run.
+ #
+ # This cannot use super(HTMLItem, self).__init__(..) because only the
+ # Collector constructor takes the fspath argument.
+ pytest.Item.__init__(self, name, parent)
+ pytest.Collector.__init__(self, name, parent, fspath=py.path.local(filename))
+
+
+ def reportinfo(self):
+ return self.fspath, None, self.url
+
+ def repr_failure(self, excinfo):
+ return pytest.Collector.repr_failure(self, excinfo)
+
+ def runtest(self):
+ if self.type == 'unit':
+ self._run_unit_test()
+ elif self.type == 'functional':
+ self._run_functional_test()
+ else:
+ raise NotImplementedError
+
+ def _run_unit_test(self):
+ driver = self.session.config.driver
+ server = self.session.config.server
+
+ driver.url = server.url(HARNESS)
+
+ actual = driver.execute_async_script(
+ 'runTest("%s", "foo", arguments[0])' % self.url
+ )
+
+ summarized = _summarize(copy.deepcopy(actual))
+
+ print(json.dumps(summarized, indent=2))
+
+ assert summarized[u'summarized_status'][u'status_string'] == u'OK', summarized[u'summarized_status'][u'message']
+ for test in summarized[u'summarized_tests']:
+ msg = "%s\n%s" % (test[u'name'], test[u'message'])
+ assert test[u'status_string'] == u'PASS', msg
+
+ def _run_functional_test(self):
+ driver = self.session.config.driver
+ server = self.session.config.server
+
+ driver.url = server.url(HARNESS)
+
+ test_url = self.url
+ actual = driver.execute_async_script('runTest("%s", "foo", arguments[0])' % test_url)
+
+ print(json.dumps(actual, indent=2))
+
+ summarized = _summarize(copy.deepcopy(actual))
+
+ print(json.dumps(summarized, indent=2))
+
+ # Test object ordering is not guaranteed. This weak assertion verifies
+ # that the indices are unique and sequential
+ indices = [test_obj.get('index') for test_obj in actual['tests']]
+ self._assert_sequence(indices)
+
+ self.expected[u'summarized_tests'].sort(key=lambda test_obj: test_obj.get('name'))
+
+ # Make asserts opt-in for now
+ if "summarized_asserts" not in self.expected:
+ del summarized["summarized_asserts"]
+ else:
+ # We can't be sure of the order of asserts even within the same test
+ # although we could also check for the failing assert being the final
+ # one
+ for obj in [summarized, self.expected]:
+ obj["summarized_asserts"].sort(
+ key=lambda x: (x["test"] or "", x["status"], x["assert_name"], tuple(x["args"])))
+
+ assert summarized == self.expected
+
+ @staticmethod
+ def _assert_sequence(nums):
+ if nums and len(nums) > 0:
+ assert nums == list(range(1, nums[-1] + 1))
diff --git a/testing/web-platform/tests/resources/test/harness.html b/testing/web-platform/tests/resources/test/harness.html
new file mode 100644
index 0000000000..5ee0f285e8
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/harness.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ </head>
+ <body>
+ <script>
+function runTest(url, id, done) {
+ var child;
+
+ function onMessage(event) {
+ if (!event.data || event.data.type !== 'complete') {
+ return;
+ }
+
+ window.removeEventListener('message', onMessage);
+ child.close();
+ done(event.data);
+ }
+ window.addEventListener('message', onMessage);
+
+ window.child = child = window.open(url, id);
+}
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/idl-helper.js b/testing/web-platform/tests/resources/test/idl-helper.js
new file mode 100644
index 0000000000..2b73527ff2
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/idl-helper.js
@@ -0,0 +1,24 @@
+"use strict";
+
+var typedefFrom = interfaceFrom;
+var dictionaryFrom = interfaceFrom;
+function interfaceFrom(i) {
+ var idl = new IdlArray();
+ idl.add_idls(i);
+ for (var prop in idl.members) {
+ return idl.members[prop];
+ }
+}
+
+function memberFrom(m) {
+ var idl = new IdlArray();
+ idl.add_idls('interface A { ' + m + '; };');
+ return idl.members["A"].members[0];
+}
+
+function typeFrom(type) {
+ var ast = WebIDL2.parse('interface Foo { ' + type + ' a(); };');
+ ast = ast[0]; // get the first fragment
+ ast = ast.members[0]; // get the first member
+ return ast.idlType; // get the type of the first field
+}
diff --git a/testing/web-platform/tests/resources/test/nested-testharness.js b/testing/web-platform/tests/resources/test/nested-testharness.js
new file mode 100644
index 0000000000..d97c1568c7
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/nested-testharness.js
@@ -0,0 +1,80 @@
+'use strict';
+
+/**
+ * Execute testharness.js and one or more scripts in an iframe. Report the
+ * results of the execution.
+ *
+ * @param {...function|...string} bodies - a function body. If specified as a
+ * function object, it will be
+ * serialized to a string using the
+ * built-in
+ * `Function.prototype.toString` prior
+ * to inclusion in the generated
+ * iframe.
+ *
+ * @returns {Promise} eventual value describing the result of the test
+ * execution; the summary object has two properties:
+ * `harness` (a string describing the harness status) and
+ * `tests` (an object whose "own" property names are the
+ * titles of the defined sub-tests and whose associated
+ * values are the subtest statuses).
+ */
+function makeTest(...bodies) {
+ const closeScript = '<' + '/script>';
+ let src = `
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Document title</title>
+<script src="/resources/testharness.js?${Math.random()}">${closeScript}
+</head>
+
+<body>
+<div id="log"></div>`;
+ bodies.forEach((body) => {
+ src += '<script>(' + body + ')();' + closeScript;
+ });
+
+ const iframe = document.createElement('iframe');
+
+ document.body.appendChild(iframe);
+ iframe.contentDocument.write(src);
+
+ return new Promise((resolve) => {
+ window.addEventListener('message', function onMessage(e) {
+ if (e.source !== iframe.contentWindow) {
+ return;
+ }
+ if (!e.data || e.data.type !=='complete') {
+ return;
+ }
+ window.removeEventListener('message', onMessage);
+ resolve(e.data);
+ });
+
+ iframe.contentDocument.close();
+ }).then(({ tests, status }) => {
+ const summary = {
+ harness: getEnumProp(status, status.status),
+ tests: {}
+ };
+
+ tests.forEach((test) => {
+ summary.tests[test.name] = getEnumProp(test, test.status);
+ });
+
+ return summary;
+ });
+}
+
+function getEnumProp(object, value) {
+ for (let property in object) {
+ if (!/^[A-Z]+$/.test(property)) {
+ continue;
+ }
+
+ if (object[property] === value) {
+ return property;
+ }
+ }
+}
diff --git a/testing/web-platform/tests/resources/test/requirements.txt b/testing/web-platform/tests/resources/test/requirements.txt
new file mode 100644
index 0000000000..95d87c3875
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/requirements.txt
@@ -0,0 +1 @@
+html5lib==1.1
diff --git a/testing/web-platform/tests/resources/test/tests/functional/abortsignal.html b/testing/web-platform/tests/resources/test/tests/functional/abortsignal.html
new file mode 100644
index 0000000000..e6080e96ec
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/abortsignal.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<title>Test#get_signal</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ "use strict";
+
+ setup(() => {
+ assert_implements_optional(window.AbortController, "No AbortController");
+ });
+
+ let signal;
+ let observed = false;
+
+ test(t => {
+ signal = t.get_signal();
+ assert_true(signal instanceof AbortSignal, "Returns an abort signal");
+ assert_false(signal.aborted, "Signal should not be aborted before test end");
+ signal.onabort = () => observed = true;
+ }, "t.signal existence");
+
+ test(t => {
+ assert_true(signal.aborted, "Signal should be aborted after test end");
+ assert_true(observed, "onabort should have been called");
+ }, "t.signal.aborted");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "message": null,
+ "name": "t.signal existence",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "t.signal.aborted",
+ "properties": {},
+ "status_string": "PASS"
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup.html
new file mode 100644
index 0000000000..468319fdbe
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+"use strict";
+
+var log_sync;
+test(function(t) {
+ log_sync = "";
+ t.add_cleanup(function() { log_sync += "1"; });
+ t.add_cleanup(function() { log_sync += "2"; });
+ t.add_cleanup(function() { log_sync += "3"; });
+ t.add_cleanup(function() { log_sync += "4"; });
+ t.add_cleanup(function() { log_sync += "5"; });
+ log_sync += "0";
+}, "probe synchronous");
+
+test(function() {
+ if (log_sync !== "012345") {
+ throw new Error("Expected: '012345'. Actual: '" + log_sync + "'.");
+ }
+}, "Cleanup methods are invoked exactly once and in the expected sequence.");
+
+var complete, log_async;
+async_test(function(t) {
+ complete = t.step_func(function() {
+ if (log_async !== "012") {
+ throw new Error("Expected: '012'. Actual: '" + log_async + "'.");
+ }
+
+ t.done();
+ });
+}, "Cleanup methods are invoked following the completion of asynchronous tests");
+
+async_test(function(t) {
+ log_async = "";
+ t.add_cleanup(function() { log_async += "1"; });
+
+ setTimeout(t.step_func(function() {
+ t.add_cleanup(function() {
+ log_async += "2";
+ complete();
+ });
+ log_async += "0";
+ t.done();
+ }), 0);
+}, "probe asynchronous");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Cleanup methods are invoked exactly once and in the expected sequence.",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Cleanup methods are invoked following the completion of asynchronous tests",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "probe asynchronous",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "probe synchronous",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async.html
new file mode 100644
index 0000000000..07ade4b93b
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup with Promise-returning functions</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+
+<script>
+"use strict";
+var completeCount = 0;
+var counts = {
+ afterTick: null,
+ afterFirst: null
+};
+
+add_result_callback(function(result_t) {
+ completeCount += 1;
+});
+
+promise_test(function(t) {
+ t.add_cleanup(function() {
+ return new Promise(function(resolve) {
+ setTimeout(function() {
+ counts.afterTick = completeCount;
+ resolve();
+ }, 0);
+ });
+ });
+ t.add_cleanup(function() {
+ return new Promise(function(resolve) {
+
+ setTimeout(function() {
+ counts.afterFirst = completeCount;
+ resolve();
+ }, 0);
+ });
+ });
+
+ return Promise.resolve();
+}, 'promise_test with asynchronous cleanup');
+
+promise_test(function() {
+ assert_equals(
+ counts.afterTick,
+ 0,
+ "test is not asynchronously considered 'complete'"
+ );
+ assert_equals(
+ counts.afterFirst,
+ 0,
+ "test is not considered 'complete' following fulfillment of first promise"
+ );
+ assert_equals(completeCount, 1);
+
+ return Promise.resolve();
+}, "synchronously-defined promise_test");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "promise_test with asynchronous cleanup",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "synchronously-defined promise_test",
+ "message": null,
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_bad_return.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_bad_return.html
new file mode 100644
index 0000000000..867bde2c39
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_bad_return.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup with non-thenable-returning function</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+
+<script>
+"use strict";
+
+promise_test(function(t) {
+ t.add_cleanup(function() {});
+ t.add_cleanup(function() {
+ return { then: 9 };
+ });
+ t.add_cleanup(function() { return Promise.resolve(); });
+
+ return Promise.resolve();
+}, "promise_test that returns a non-thenable object in one \"cleanup\" callback");
+
+promise_test(function() {}, "The test runner is in an unpredictable state ('NOT RUN')");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Test named 'promise_test that returns a non-thenable object in one \"cleanup\" callback' specified 3 'cleanup' functions, and 1 returned a non-thenable value."
+ },
+ "summarized_tests": [
+ {
+ "status_string": "NOTRUN",
+ "name": "The test runner is in an unpredictable state ('NOT RUN')",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "promise_test that returns a non-thenable object in one \"cleanup\" callback",
+ "message": null,
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_rejection.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_rejection.html
new file mode 100644
index 0000000000..e51465e7eb
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_rejection.html
@@ -0,0 +1,94 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup with Promise-returning functions (rejection handling)</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+
+<script>
+"use strict";
+var resolve, reject;
+var completeCount = 0;
+add_result_callback(function(result_t) {
+ completeCount += 1;
+});
+promise_test(function(t) {
+ t.add_cleanup(function() {
+ return new Promise(function(_, _reject) { reject = _reject; });
+ });
+ t.add_cleanup(function() {
+ return new Promise(function(_resolve) { resolve = _resolve; });
+ });
+
+ // The following cleanup function defines empty tests so that the reported
+ // data demonstrates the intended run-time behavior without relying on the
+ // test harness's handling of errors during test cleanup (which is tested
+ // elsewhere).
+ t.add_cleanup(function() {
+ if (completeCount === 0) {
+ promise_test(
+ function() {},
+ "test is not asynchronously considered 'complete' ('NOT RUN')"
+ );
+ }
+
+ reject();
+
+ setTimeout(function() {
+ if (completeCount === 0) {
+ promise_test(
+ function() {},
+ "test is not considered 'complete' following rejection of first " +
+ "promise ('NOT RUN')"
+ );
+ }
+
+ resolve();
+ }, 0);
+ });
+
+ return Promise.resolve();
+}, "promise_test with asynchronous cleanup including rejection");
+
+promise_test(function() {}, "synchronously-defined test ('NOT RUN')");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Test named 'promise_test with asynchronous cleanup including rejection' specified 3 'cleanup' functions, and 1 failed."
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "promise_test with asynchronous cleanup including rejection",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "synchronously-defined test ('NOT RUN')",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "test is not asynchronously considered 'complete' ('NOT RUN')",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "test is not considered 'complete' following rejection of first promise ('NOT RUN')",
+ "message": null,
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_rejection_after_load.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_rejection_after_load.html
new file mode 100644
index 0000000000..f9b2846100
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_rejection_after_load.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup with Promise-returning functions (rejection handling following "load" event)</title>
+</head>
+<body>
+<h1>Promise Tests</h1>
+<p>This test demonstrates the use of <tt>promise_test</tt>. Assumes ECMAScript 6
+Promise support. Some failures are expected.</p>
+<div id="log"></div>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+<script>
+promise_test(function(t) {
+ t.add_cleanup(function() {
+ return Promise.reject(new Error("foo"));
+ });
+
+ return new Promise((resolve) => {
+ document.addEventListener("DOMContentLoaded", function() {
+ setTimeout(resolve, 0)
+ });
+ });
+}, "Test with failing cleanup that completes after DOMContentLoaded event");
+
+promise_test(function(t) {
+ return Promise.resolve();
+}, "Test that should not be run due to invalid harness state ('NOT RUN')");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Test named 'Test with failing cleanup that completes after DOMContentLoaded event' specified 1 'cleanup' function, and 1 failed."
+ },
+ "summarized_tests": [
+ {
+ "status_string": "NOTRUN",
+ "name": "Test that should not be run due to invalid harness state ('NOT RUN')",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test with failing cleanup that completes after DOMContentLoaded event",
+ "message": null,
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_timeout.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_timeout.html
new file mode 100644
index 0000000000..429536ce6e
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_timeout.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup with Promise-returning functions (timeout handling)</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+"use strict";
+
+promise_test(function(t) {
+ t.add_cleanup(function() {
+ return Promise.resolve();
+ });
+
+ t.add_cleanup(function() {
+ return new Promise(function() {});
+ });
+
+ t.add_cleanup(function() {});
+
+ t.add_cleanup(function() {
+ return new Promise(function() {});
+ });
+
+ return Promise.resolve();
+}, "promise_test with asynchronous cleanup");
+
+promise_test(function() {}, "promise_test following timed out cleanup ('NOT RUN')");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Timeout while running cleanup for test named \"promise_test with asynchronous cleanup\"."
+ },
+ "summarized_tests": [
+ {
+ "status_string": "NOTRUN",
+ "name": "promise_test following timed out cleanup ('NOT RUN')",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "promise_test with asynchronous cleanup",
+ "message": null,
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_bad_return.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_bad_return.html
new file mode 100644
index 0000000000..3cfb28adf2
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_bad_return.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup with value-returning function</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+"use strict";
+
+test(function(t) {
+ t.add_cleanup(function() {});
+ t.add_cleanup(function() { return null; });
+ t.add_cleanup(function() {
+ test(
+ function() {},
+ "The test runner is in an unpredictable state #1 ('NOT RUN')"
+ );
+
+ throw new Error();
+ });
+ t.add_cleanup(function() { return 4; });
+ t.add_cleanup(function() { return { then: function() {} }; });
+ t.add_cleanup(function() {});
+}, "Test that returns a value in three \"cleanup\" functions");
+
+test(function() {}, "The test runner is in an unpredictable state #2 ('NOT RUN')");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Test named 'Test that returns a value in three \"cleanup\" functions' specified 6 'cleanup' functions, and 1 failed, and 3 returned a non-undefined value."
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Test that returns a value in three \"cleanup\" functions",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "The test runner is in an unpredictable state #1 ('NOT RUN')",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "The test runner is in an unpredictable state #2 ('NOT RUN')",
+ "message": null,
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_count.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_count.html
new file mode 100644
index 0000000000..2c9b51c6d0
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_count.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup reported count</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+
+<script>
+promise_test(function(t) {
+ t.add_cleanup(function() {});
+ t.add_cleanup(function() {});
+ t.add_cleanup(function() { throw new Error(); });
+ new EventWatcher(t, document.body, []);
+
+ return Promise.resolve();
+}, 'test with 3 user-defined cleanup functions');
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Test named 'test with 3 user-defined cleanup functions' specified 3 'cleanup' functions, and 1 failed."
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "test with 3 user-defined cleanup functions",
+ "message": null,
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_err.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_err.html
new file mode 100644
index 0000000000..60357c66ee
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_err.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup: error</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+"use strict";
+
+test(function(t) {
+ t.add_cleanup(function() {
+ throw new Error('exception in cleanup function');
+ });
+}, "Exception in cleanup function causes harness failure.");
+
+test(function() {}, "This test should not be run.");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Test named 'Exception in cleanup function causes harness failure.' specified 1 'cleanup' function, and 1 failed."
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Exception in cleanup function causes harness failure.",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "This test should not be run.",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_err_multi.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_err_multi.html
new file mode 100644
index 0000000000..80ba1b4959
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_err_multi.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup: multiple functions with one in error</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+
+<script>
+"use strict";
+
+test(function(t) {
+ t.add_cleanup(function() {
+ throw new Error("exception in cleanup function");
+ });
+
+ // The following cleanup function defines a test so that the reported
+ // data demonstrates the intended run-time behavior, i.e. that
+ // `testharness.js` invokes all cleanup functions even when one or more
+ // throw errors.
+ t.add_cleanup(function() {
+ test(function() {}, "Verification test");
+ });
+ }, "Test with multiple cleanup functions");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Test named 'Test with multiple cleanup functions' specified 2 'cleanup' functions, and 1 failed."
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Test with multiple cleanup functions",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "Verification test",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_sync_queue.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_sync_queue.html
new file mode 100644
index 0000000000..0a61503cc8
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_sync_queue.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#add_cleanup: queuing tests</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+
+<script>
+"use strict";
+var firstCleaned = false;
+
+promise_test(function(t) {
+ promise_test(function() {
+ assert_true(
+ firstCleaned, "should not execute until first test is complete"
+ );
+
+ return Promise.resolve();
+ }, "test defined when no tests are queued, but one test is executing");
+
+ t.add_cleanup(function() {
+ firstCleaned = true;
+ });
+
+ return Promise.resolve();
+}, "Test with a 'cleanup' function");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "message": null,
+ "name": "Test with a 'cleanup' function",
+ "status_string": "PASS",
+ "properties": {}
+ },
+ {
+ "message": null,
+ "name": "test defined when no tests are queued, but one test is executing",
+ "status_string": "PASS",
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/api-tests-1.html b/testing/web-platform/tests/resources/test/tests/functional/api-tests-1.html
new file mode 100644
index 0000000000..9de875b0f1
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/api-tests-1.html
@@ -0,0 +1,991 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Sample HTML5 API Tests</title>
+<meta name="timeout" content="6000">
+</head>
+<body onload="load_test_attr.done()">
+<h1>Sample HTML5 API Tests</h1>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ setup_run = false;
+ setup(function() {
+ setup_run = true;
+ });
+ test(function() {assert_true(setup_run)}, "Setup function ran");
+
+ // Two examples for testing events from handler and attributes
+ var load_test_event = async_test("window onload event fires when set from the handler");
+
+ function windowLoad()
+ {
+ load_test_event.done();
+ }
+ on_event(window, "load", windowLoad);
+
+ test(function() {
+ var sequence = [];
+ var outer = document.createElement("div");
+ var inner = document.createElement("div");
+ outer.appendChild(inner);
+ document.body.appendChild(outer);
+ inner.addEventListener("click", function() {
+ sequence.push("inner");
+ }, false);
+
+ on_event(outer, "click", function() {
+ sequence.push("outer");
+ });
+ inner.click();
+
+ assert_array_equals(sequence, ["inner", "outer"]);
+ }, "on_event does not use event capture");
+
+ // see the body onload below
+ var load_test_attr = async_test("body element fires the onload event set from the attribute");
+</script>
+<script>
+ function bodyElement()
+ {
+ assert_equals(document.body, document.getElementsByTagName("body")[0]);
+ }
+ test(bodyElement, "document.body should be the first body element in the document");
+
+ test(function() {
+ assert_equals(1,1);
+ assert_equals(NaN, NaN, "NaN case");
+ assert_equals(0, 0, "Zero case");
+ }, "assert_equals tests")
+
+ test(function() {
+ assert_equals(-0, 0, "Zero case");
+ }, "assert_equals tests expected to fail")
+
+ test(function() {
+ assert_not_equals({}, {}, "object case");
+ assert_not_equals(-0, 0, "Zero case");
+ }, "assert_not_equals tests")
+
+ function testAssertPass()
+ {
+ assert_true(true);
+ }
+ test(testAssertPass, "assert_true expected to pass");
+
+ function testAssertFalse()
+ {
+ assert_true(false, "false should not be true");
+ }
+ test(testAssertFalse, "assert_true expected to fail");
+
+ function basicAssertArrayEquals()
+ {
+ assert_array_equals([1, NaN], [1, NaN], "[1, NaN] is equal to [1, NaN]");
+ }
+ test(basicAssertArrayEquals, "basic assert_array_equals test");
+
+ function assertArrayEqualsUndefined()
+ {
+ assert_array_equals(undefined, [1], "undefined equals [1]?");
+ }
+ test(assertArrayEqualsUndefined, "assert_array_equals with first param undefined");
+
+ function assertArrayEqualsTrue()
+ {
+ assert_array_equals(true, [1], "true equals [1]?");
+ }
+ test(assertArrayEqualsTrue, "assert_array_equals with first param true");
+
+ function assertArrayEqualsFalse()
+ {
+ assert_array_equals(false, [1], "false equals [1]?");
+ }
+ test(assertArrayEqualsFalse, "assert_array_equals with first param false");
+
+ function assertArrayEqualsNull()
+ {
+ assert_array_equals(null, [1], "null equals [1]?");
+ }
+ test(assertArrayEqualsNull, "assert_array_equals with first param null");
+
+ function assertArrayEqualsNumeric()
+ {
+ assert_array_equals(1, [1], "1 equals [1]?");
+ }
+ test(assertArrayEqualsNumeric, "assert_array_equals with first param 1");
+
+ function basicAssertArrayApproxEquals()
+ {
+ assert_array_approx_equals([10, 11], [11, 10], 1, "[10, 11] is approximately (+/- 1) [11, 10]")
+ }
+ test(basicAssertArrayApproxEquals, "basic assert_array_approx_equals test");
+
+ function basicAssertApproxEquals()
+ {
+ assert_approx_equals(10, 11, 1, "10 is approximately (+/- 1) 11")
+ }
+ test(basicAssertApproxEquals, "basic assert_approx_equals test");
+
+ function basicAssertLessThan()
+ {
+ assert_less_than(10, 11, "10 is less than 11")
+ }
+ test(basicAssertApproxEquals, "basic assert_less_than test");
+
+ function basicAssertGreaterThan()
+ {
+ assert_greater_than(10, 11, "10 is not greater than 11");
+ }
+ test(basicAssertGreaterThan, "assert_greater_than expected to fail");
+
+ function basicAssertGreaterThanEqual()
+ {
+ assert_greater_than_equal(10, 10, "10 is greater than or equal to 10")
+ }
+ test(basicAssertGreaterThanEqual, "basic assert_greater_than_equal test");
+
+ function basicAssertLessThanEqual()
+ {
+ assert_greater_than_equal('10', 10, "'10' is not a number")
+ }
+ test(basicAssertLessThanEqual, "assert_less_than_equal expected to fail");
+
+ function testAssertInherits() {
+ var A = function(){this.a = "a"}
+ A.prototype = {b:"b"}
+ var a = new A();
+ assert_own_property(a, "a");
+ assert_not_own_property(a, "b", "unexpected property found: \"b\"");
+ assert_inherits(a, "b");
+ }
+ test(testAssertInherits, "test for assert[_not]_own_property and insert_inherits")
+
+ test(function()
+ {
+ var a = document.createElement("a")
+ var b = document.createElement("b")
+ assert_throws_dom("NOT_FOUND_ERR", function () {a.removeChild(b)});
+ }, "Test throw DOM exception")
+
+ test(function()
+ {
+ var a = document.createElement("a")
+ var b = document.createElement("b")
+ assert_throws_js(DOMException, function () {a.removeChild(b)});
+ }, "Test throw DOMException as JS exception expected to fail")
+
+ test(function()
+ {
+ assert_throws_js(SyntaxError, function () {document.querySelector("")});
+ }, "Test throw SyntaxError DOMException where JS SyntaxError expected; expected to fail")
+
+ test(function()
+ {
+ assert_throws_js(SyntaxError, function () {JSON.parse("{")});
+ }, "Test throw JS SyntaxError")
+
+ test(function()
+ {
+ assert_throws_dom("SyntaxError", function () {document.querySelector("")});
+ }, "Test throw DOM SyntaxError")
+
+ test(function()
+ {
+ var ifr = document.createElement("iframe");
+ document.body.appendChild(ifr);
+ this.add_cleanup(() => ifr.remove());
+ assert_throws_dom("SyntaxError", ifr.contentWindow.DOMException,
+ function () {ifr.contentDocument.querySelector("")});
+ }, "Test throw DOM SyntaxError from subframe");
+
+ test(function()
+ {
+ var ifr = document.createElement("iframe");
+ document.body.appendChild(ifr);
+ this.add_cleanup(() => ifr.remove());
+ assert_throws_dom("SyntaxError",
+ function () {ifr.contentDocument.querySelector("")});
+ }, "Test throw DOM SyntaxError from subframe with incorrect global expectation; expected to fail");
+
+ test(function()
+ {
+ var ifr = document.createElement("iframe");
+ document.body.appendChild(ifr);
+ this.add_cleanup(() => ifr.remove());
+ assert_throws_dom("SyntaxError", ifr.contentWindow.DOMException,
+ function () {document.querySelector("")});
+ }, "Test throw DOM SyntaxError with incorrect expectation; expected to fail");
+
+ test(function()
+ {
+ assert_throws_dom("SyntaxError", function () {JSON.parse("{")});
+ }, "Test throw JS SyntaxError where SyntaxError DOMException expected; expected to fail")
+
+ test(function()
+ {
+ var a = document.createTextNode("a")
+ var b = document.createElement("b")
+ assert_throws_dom("NOT_FOUND_ERR", function () {a.appendChild(b)});
+ }, "Test throw DOM exception expected to fail")
+
+ test(function()
+ {
+ var e = new DOMException("I am not known", "TEST_ERROR_NO_SUCH_THING");
+ assert_throws_dom(0, function() {throw e});
+ }, "Test assert_throws_dom with ambiguous DOM-exception expected to Fail");
+
+ test(function()
+ {
+ var e = {code:0, name:"TEST_ERR", TEST_ERR:0};
+ e.constructor = DOMException;
+ assert_throws_dom("TEST_ERR", function() {throw e});
+ }, "Test assert_throws_dom with non-DOM-exception expected to Fail");
+
+ test(function()
+ {
+ var e = {code: DOMException.SYNTAX_ERR, name:"SyntaxError"};
+ e.constructor = DOMException;
+ assert_throws_dom(DOMException.SYNTAX_ERR, function() {throw e});
+ }, "Test assert_throws_dom with number code value expected to Pass");
+
+ test(function()
+ {
+ var e = new DOMException("Some message", "SyntaxError");
+ assert_throws_dom(DOMException.SYNTAX_ERR, function() {throw e});
+ }, "Test assert_throws_dom with number code value and real DOMException expected to Pass");
+
+ var t = async_test("Test step_func")
+ setTimeout(
+ t.step_func(
+ function () {
+ assert_true(true); t.done();
+ }), 0);
+
+ async_test(function(t) {
+ setTimeout(t.step_func(function (){assert_true(true); t.done();}), 0);
+ }, "Test async test with callback");
+
+ async_test(function() {
+ setTimeout(this.step_func(function (){assert_true(true); this.done();}), 0);
+ }, "Test async test with callback and `this` obj.");
+
+ async_test("test should timeout (fail) with the default of 2 seconds").step(function(){});
+
+ async_test("async test that is never started, should have status Not Run");
+
+
+ test(function(t) {
+ window.global = 1;
+ t.add_cleanup(function() {delete window.global});
+ assert_equals(window.global, 1);
+ },
+ "Test that defines a global and cleans it up");
+
+ test(function() {assert_equals(window.global, undefined)},
+ "Test that cleanup handlers from previous test ran");
+
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "TIMEOUT",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Setup function ran",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Test assert_throws_dom with ambiguous DOM-exception expected to Fail",
+ "message": "Test bug: ambiguous DOMException code 0 passed to assert_throws_dom()",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Test assert_throws_dom with non-DOM-exception expected to Fail",
+ "message": "Test bug: unrecognized DOMException code name or name \"TEST_ERR\" passed to assert_throws_dom()",
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test assert_throws_dom with number code value expected to Pass",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test assert_throws_dom with number code value and real DOMException expected to Pass",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test async test with callback",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test async test with callback and `this` obj.",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test step_func",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test that cleanup handlers from previous test ran",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test that defines a global and cleans it up",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test throw DOM exception",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Test throw DOMException as JS exception expected to fail",
+ "message": "assert_throws_js: function \"function DOMException() {\n [native code]\n}\" is not an Error subtype",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Test throw SyntaxError DOMException where JS SyntaxError expected; expected to fail",
+ "message": "assert_throws_js: function \"function () {document.querySelector(\"\")}\" threw object \"SyntaxError: Document.querySelector: '' is not a valid selector\" (\"SyntaxError\") expected instance of function \"function SyntaxError() {\n [native code]\n}\" (\"SyntaxError\")",
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test throw JS SyntaxError",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test throw DOM SyntaxError",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test throw DOM SyntaxError from subframe",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Test throw DOM SyntaxError from subframe with incorrect global expectation; expected to fail",
+ "message": "assert_throws_dom: function \"function () {ifr.contentDocument.querySelector(\"\")}\" threw an exception from the wrong global",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Test throw DOM SyntaxError with incorrect expectation; expected to fail",
+ "message": "assert_throws_dom: function \"function () {document.querySelector(\"\")}\" threw an exception from the wrong global",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Test throw JS SyntaxError where SyntaxError DOMException expected; expected to fail",
+ "message": "assert_throws_dom: function \"function () {JSON.parse(\"{\")}\" threw object \"SyntaxError: JSON.parse: end of data while reading object contents at line 1 column 2 of the JSON data\" that is not a DOMException SyntaxError: property \"code\" is equal to undefined, expected 12",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Test throw DOM exception expected to fail",
+ "message": "assert_throws_dom: function \"function () {a.appendChild(b)}\" threw object \"HierarchyRequestError: Node.appendChild: Cannot add children to a Text\" that is not a DOMException NOT_FOUND_ERR: property \"code\" is equal to 3, expected 8",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "assert_array_equals with first param 1",
+ "message": "assert_array_equals: 1 equals [1]? value is 1, expected array",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "assert_array_equals with first param false",
+ "message": "assert_array_equals: false equals [1]? value is false, expected array",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "assert_array_equals with first param null",
+ "message": "assert_array_equals: null equals [1]? value is null, expected array",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "assert_array_equals with first param true",
+ "message": "assert_array_equals: true equals [1]? value is true, expected array",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "assert_array_equals with first param undefined",
+ "message": "assert_array_equals: undefined equals [1]? value is undefined, expected array",
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_equals tests",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "assert_equals tests expected to fail",
+ "message": "assert_equals: Zero case expected 0 but got -0",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "assert_greater_than expected to fail",
+ "message": "assert_greater_than: 10 is not greater than 11 expected a number greater than 11 but got 10",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "assert_less_than_equal expected to fail",
+ "message": "assert_greater_than_equal: '10' is not a number expected a number but got a \"string\"",
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_not_equals tests",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "assert_true expected to fail",
+ "message": "assert_true: false should not be true expected true got false",
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_true expected to pass",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "async test that is never started, should have status Not Run",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "basic assert_approx_equals test",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "basic assert_array_approx_equals test",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "basic assert_array_equals test",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "basic assert_greater_than_equal test",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "basic assert_less_than test",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "body element fires the onload event set from the attribute",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "document.body should be the first body element in the document",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "test for assert[_not]_own_property and insert_inherits",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "TIMEOUT",
+ "name": "test should timeout (fail) with the default of 2 seconds",
+ "message": "Test timed out",
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "window onload event fires when set from the handler",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "on_event does not use event capture",
+ "message": null,
+ "properties": {}
+ }
+ ],
+ "summarized_asserts": [
+ {
+ "assert_name": "assert_true",
+ "test": "Setup function ran",
+ "args": [
+ "true"
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_array_equals",
+ "test": "on_event does not use event capture",
+ "args": [
+ "[\"inner\", \"outer\"]",
+ "[\"inner\", \"outer\"]"
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_equals",
+ "test": "document.body should be the first body element in the document",
+ "args": [
+ "Element node <body onload=\"load_test_attr.done()\"> <h1>Sample HTML5 AP...",
+ "Element node <body onload=\"load_test_attr.done()\"> <h1>Sample HTML5 AP..."
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_equals",
+ "test": "assert_equals tests",
+ "args": [
+ "1",
+ "1"
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_equals",
+ "test": "assert_equals tests",
+ "args": [
+ "NaN",
+ "NaN",
+ "\"NaN case\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_equals",
+ "test": "assert_equals tests",
+ "args": [
+ "0",
+ "0",
+ "\"Zero case\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_equals",
+ "test": "assert_equals tests expected to fail",
+ "args": [
+ "-0",
+ "0",
+ "\"Zero case\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_not_equals",
+ "test": "assert_not_equals tests",
+ "args": [
+ "object \"[object Object]\"",
+ "object \"[object Object]\"",
+ "\"object case\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_not_equals",
+ "test": "assert_not_equals tests",
+ "args": [
+ "-0",
+ "0",
+ "\"Zero case\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_true",
+ "test": "assert_true expected to pass",
+ "args": [
+ "true"
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_true",
+ "test": "assert_true expected to fail",
+ "args": [
+ "false",
+ "\"false should not be true\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_array_equals",
+ "test": "basic assert_array_equals test",
+ "args": [
+ "[1, NaN]",
+ "[1, NaN]",
+ "\"[1, NaN] is equal to [1, NaN]\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_array_equals",
+ "test": "assert_array_equals with first param undefined",
+ "args": [
+ "undefined",
+ "[1]",
+ "\"undefined equals [1]?\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_array_equals",
+ "test": "assert_array_equals with first param true",
+ "args": [
+ "true",
+ "[1]",
+ "\"true equals [1]?\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_array_equals",
+ "test": "assert_array_equals with first param false",
+ "args": [
+ "false",
+ "[1]",
+ "\"false equals [1]?\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_array_equals",
+ "test": "assert_array_equals with first param null",
+ "args": [
+ "null",
+ "[1]",
+ "\"null equals [1]?\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_array_equals",
+ "test": "assert_array_equals with first param 1",
+ "args": [
+ "1",
+ "[1]",
+ "\"1 equals [1]?\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_array_approx_equals",
+ "test": "basic assert_array_approx_equals test",
+ "args": [
+ "[10, 11]",
+ "[11, 10]",
+ "1",
+ "\"[10, 11] is approximately (+/- 1) [11, 10]\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_approx_equals",
+ "test": "basic assert_approx_equals test",
+ "args": [
+ "10",
+ "11",
+ "1",
+ "\"10 is approximately (+/- 1) 11\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_approx_equals",
+ "test": "basic assert_less_than test",
+ "args": [
+ "10",
+ "11",
+ "1",
+ "\"10 is approximately (+/- 1) 11\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_greater_than",
+ "test": "assert_greater_than expected to fail",
+ "args": [
+ "10",
+ "11",
+ "\"10 is not greater than 11\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_greater_than_equal",
+ "test": "basic assert_greater_than_equal test",
+ "args": [
+ "10",
+ "10",
+ "\"10 is greater than or equal to 10\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_greater_than_equal",
+ "test": "assert_less_than_equal expected to fail",
+ "args": [
+ "\"10\"",
+ "10",
+ "\"'10' is not a number\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_own_property",
+ "test": "test for assert[_not]_own_property and insert_inherits",
+ "args": [
+ "object \"[object Object]\"",
+ "\"a\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_not_own_property",
+ "test": "test for assert[_not]_own_property and insert_inherits",
+ "args": [
+ "object \"[object Object]\"",
+ "\"b\"",
+ "\"unexpected property found: \\\"b\\\"\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_inherits",
+ "test": "test for assert[_not]_own_property and insert_inherits",
+ "args": [
+ "object \"[object Object]\"",
+ "\"b\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test throw DOM exception",
+ "args": [
+ "\"NOT_FOUND_ERR\"",
+ "function \"function () {a.removeChild(b)}\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_throws_js",
+ "test": "Test throw DOMException as JS exception expected to fail",
+ "args": [
+ "function \"function DOMException() { [native code] }\"",
+ "function \"function () {a.removeChild(b)}\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_throws_js",
+ "test": "Test throw SyntaxError DOMException where JS SyntaxError expected; expected to fail",
+ "args": [
+ "function \"function SyntaxError() { [native code] }\"",
+ "function \"function () {document.querySelector(\"\")}\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_throws_js",
+ "test": "Test throw JS SyntaxError",
+ "args": [
+ "function \"function SyntaxError() { [native code] }\"",
+ "function \"function () {JSON.parse(\"{\")}\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test throw DOM SyntaxError",
+ "args": [
+ "\"SyntaxError\"",
+ "function \"function () {document.querySelector(\"\")}\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test throw DOM SyntaxError from subframe",
+ "args": [
+ "\"SyntaxError\"",
+ "function \"function DOMException() { [native code] }\"",
+ "function \"function () {ifr.contentDocument.querySelector(\"\")}\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test throw DOM SyntaxError from subframe with incorrect global expectation; expected to fail",
+ "args": [
+ "\"SyntaxError\"",
+ "function \"function () {ifr.contentDocument.querySelector(\"\")}\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test throw DOM SyntaxError with incorrect expectation; expected to fail",
+ "args": [
+ "\"SyntaxError\"",
+ "function \"function DOMException() { [native code] }\"",
+ "function \"function () {document.querySelector(\"\")}\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test throw JS SyntaxError where SyntaxError DOMException expected; expected to fail",
+ "args": [
+ "\"SyntaxError\"",
+ "function \"function () {JSON.parse(\"{\")}\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test throw DOM exception expected to fail",
+ "args": [
+ "\"NOT_FOUND_ERR\"",
+ "function \"function () {a.appendChild(b)}\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test assert_throws_dom with ambiguous DOM-exception expected to Fail",
+ "args": [
+ "0",
+ "function \"function() {throw e}\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test assert_throws_dom with non-DOM-exception expected to Fail",
+ "args": [
+ "\"TEST_ERR\"",
+ "function \"function() {throw e}\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test assert_throws_dom with number code value expected to Pass",
+ "args": [
+ "12",
+ "function \"function() {throw e}\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Test assert_throws_dom with number code value and real DOMException expected to Pass",
+ "args": [
+ "12",
+ "function \"function() {throw e}\""
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_equals",
+ "test": "Test that defines a global and cleans it up",
+ "args": [
+ "1",
+ "1"
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_equals",
+ "test": "Test that cleanup handlers from previous test ran",
+ "args": [
+ "undefined",
+ "undefined"
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_true",
+ "test": "Test step_func",
+ "args": [
+ "true"
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_true",
+ "test": "Test async test with callback",
+ "args": [
+ "true"
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_true",
+ "test": "Test async test with callback and `this` obj.",
+ "args": [
+ "true"
+ ],
+ "status": 0
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/api-tests-2.html b/testing/web-platform/tests/resources/test/tests/functional/api-tests-2.html
new file mode 100644
index 0000000000..9af94f61ac
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/api-tests-2.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Sample HTML5 API Tests</title>
+</head>
+<body>
+<h1>Sample HTML5 API Tests</h1>
+<p>There should be two results</p>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+setup({explicit_done:true})
+test(function() {assert_true(true)}, "Test defined before onload");
+
+onload = function() {test(function (){assert_true(true)}, "Test defined after onload");
+done();
+}
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Test defined after onload",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test defined before onload",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "summarized_asserts": [
+ {
+ "assert_name": "assert_true",
+ "test": "Test defined before onload",
+ "args": [
+ "true"
+ ],
+ "status": 0
+ },
+ {
+ "assert_name": "assert_true",
+ "test": "Test defined after onload",
+ "args": [
+ "true"
+ ],
+ "status": 0
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/api-tests-3.html b/testing/web-platform/tests/resources/test/tests/functional/api-tests-3.html
new file mode 100644
index 0000000000..991fc6da67
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/api-tests-3.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Sample HTML5 API Tests</title>
+</head>
+<script src="/resources/testharness.js"></script>
+
+<body>
+<h1>Sample HTML5 API Tests</h1>
+<div id="log"></div>
+<script>
+setup({explicit_timeout:true});
+var t = async_test("This test should give a status of 'Not Run' without a delay");
+timeout();
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "TIMEOUT",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "NOTRUN",
+ "name": "This test should give a status of 'Not Run' without a delay",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/assert-array-equals.html b/testing/web-platform/tests/resources/test/tests/functional/assert-array-equals.html
new file mode 100644
index 0000000000..b6460a4868
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/assert-array-equals.html
@@ -0,0 +1,162 @@
+<!DOCTYPE HTML>
+<title>assert_array_equals</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+test(() => {
+ assert_array_equals([], []);
+}, "empty and equal");
+test(() => {
+ assert_array_equals([1], [1]);
+}, "non-empty and equal");
+test(() => {
+ assert_array_equals([], [1]);
+}, "length differs");
+test(() => {
+ assert_array_equals([1], [,]);
+}, "property is present");
+test(() => {
+ assert_array_equals([,], [1]);
+}, "property is missing");
+test(() => {
+ assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20], ["x",1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]);
+}, "property 0 differs");
+test(() => {
+ assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20], [0,1,2,3,4,"x",6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]);
+}, "property 5 differs");
+test(() => {
+ assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21], [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]);
+}, "lengths differ and input array beyond display limit");
+test(() => {
+ assert_array_equals([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]);
+}, "lengths differ and expected array beyond display limit");
+test(() => {
+ assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21], [,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]);
+}, "property 0 is present and arrays are beyond display limit");
+test(() => {
+ assert_array_equals([,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]);
+}, "property 0 is missing and arrays are beyond display limit");
+test(() => {
+ assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,,19,20,21]);
+}, "property 18 is present and arrays are beyond display limit");
+test(() => {
+ assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,,19,20,21], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]);
+}, "property 18 is missing and arrays are beyond display limit");
+test(() => {
+ assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21], ["x",1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]);
+}, "property 0 differs and arrays are beyond display limit");
+test(() => {
+ assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21], [0,1,2,3,4,"x",6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]);
+}, "property 5 differs and arrays are beyond display limit");
+test(() => {
+ assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26], [0,1,2,3,4,5,6,7,8,9,10,11,"x",13,14,15,16,17,18,19,20,21,22,23,24,25,26]);
+}, "property 5 differs and arrays are beyond display limit on both sides");
+</script>
+<script type="text/json" id="expected">
+{
+ "type": "complete",
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "name": "empty and equal",
+ "message": null,
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "name": "length differs",
+ "message": "assert_array_equals: lengths differ, expected array [1] length 1, got [] length 0",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "lengths differ and expected array beyond display limit",
+ "message": "assert_array_equals: lengths differ, expected array [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21] length 22, got [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21] length 21",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "lengths differ and input array beyond display limit",
+ "message": "assert_array_equals: lengths differ, expected array [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21] length 21, got [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21] length 22",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "non-empty and equal",
+ "message": null,
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "name": "property 0 differs",
+ "message": "assert_array_equals: expected property 0 to be \"x\" but got 0 (expected array [\"x\", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] got [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property 0 differs and arrays are beyond display limit",
+ "message": "assert_array_equals: expected property 0 to be \"x\" but got 0 (expected array [\"x\", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026] got [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property 0 is missing and arrays are beyond display limit",
+ "message": "assert_array_equals: expected property 0 to be \"present\" but was \"missing\" (expected array [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026] got [, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property 0 is present and arrays are beyond display limit",
+ "message": "assert_array_equals: expected property 0 to be \"missing\" but was \"present\" (expected array [, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026] got [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property 18 is missing and arrays are beyond display limit",
+ "message": "assert_array_equals: expected property 18 to be \"present\" but was \"missing\" (expected array [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21] got [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, , 19, 20, 21])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property 18 is present and arrays are beyond display limit",
+ "message": "assert_array_equals: expected property 18 to be \"missing\" but was \"present\" (expected array [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, , 19, 20, 21] got [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property 5 differs",
+ "message": "assert_array_equals: expected property 5 to be \"x\" but got 5 (expected array [0, 1, 2, 3, 4, \"x\", 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] got [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property 5 differs and arrays are beyond display limit",
+ "message": "assert_array_equals: expected property 5 to be \"x\" but got 5 (expected array [0, 1, 2, 3, 4, \"x\", 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026] got [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property 5 differs and arrays are beyond display limit on both sides",
+ "message": "assert_array_equals: expected property 12 to be \"x\" but got 12 (expected array [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, \"x\", 13, 14, 15, 16, 17, 18, 19, 20, 21, \u2026] got [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, \u2026])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property is missing",
+ "message": "assert_array_equals: expected property 0 to be \"present\" but was \"missing\" (expected array [1] got [])",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "name": "property is present",
+ "message": "assert_array_equals: expected property 0 to be \"missing\" but was \"present\" (expected array [] got [1])",
+ "properties": {},
+ "status_string": "FAIL"
+ }
+ ]
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/assert-throws-dom.html b/testing/web-platform/tests/resources/test/tests/functional/assert-throws-dom.html
new file mode 100644
index 0000000000..4dd66b2372
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/assert-throws-dom.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<title>assert_throws_dom</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+test(() => {
+ function f() {
+ assert_true(false, "Trivial assertion.");
+
+ // Would lead to throwing a SyntaxError.
+ document.createElement("div").contentEditable = "invalid";
+ }
+
+ assert_throws_dom("SyntaxError", () => { f(); });
+}, "Violated assertion nested in `assert_throws_dom`.");
+</script>
+<script type="text/json" id="expected">
+{
+ "type": "complete",
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "message": "assert_true: Trivial assertion. expected true got false",
+ "name": "Violated assertion nested in `assert_throws_dom`.",
+ "properties": {},
+ "status_string": "FAIL"
+ }
+ ],
+ "summarized_asserts": [
+ {
+ "assert_name": "assert_throws_dom",
+ "test": "Violated assertion nested in `assert_throws_dom`.",
+ "args": [
+ "\"SyntaxError\"",
+ "function \"() => { f(); }\""
+ ],
+ "status": 1
+ },
+ {
+ "assert_name": "assert_true",
+ "test": "Violated assertion nested in `assert_throws_dom`.",
+ "args": [
+ "false",
+ "\"Trivial assertion.\""
+ ],
+ "status": 1
+ }
+ ]
+}
+</script>
+
diff --git a/testing/web-platform/tests/resources/test/tests/functional/force_timeout.html b/testing/web-platform/tests/resources/test/tests/functional/force_timeout.html
new file mode 100644
index 0000000000..2058fdb862
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/force_timeout.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test#force_timeout</title>
+</head>
+<body>
+<h1>Test#force_timeout</h1>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+setup({ explicit_timeout: true });
+
+test(function(t) {
+ t.force_timeout();
+ }, 'test (synchronous)');
+
+async_test(function(t) {
+ t.step_timeout(function() {
+ t.force_timeout();
+ }, 0);
+ }, 'async_test');
+
+promise_test(function(t) {
+ t.force_timeout();
+
+ return new Promise(function() {});
+ }, 'promise_test');
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "TIMEOUT",
+ "name": "async_test",
+ "message": "Test timed out",
+ "properties": {}
+ },
+ {
+ "status_string": "TIMEOUT",
+ "name": "promise_test",
+ "message": "Test timed out",
+ "properties": {}
+ },
+ {
+ "status_string": "TIMEOUT",
+ "name": "test (synchronous)",
+ "message": "Test timed out",
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/generate-callback.html b/testing/web-platform/tests/resources/test/tests/functional/generate-callback.html
new file mode 100644
index 0000000000..11d41743b3
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/generate-callback.html
@@ -0,0 +1,153 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Sample for using generate_tests to create a series of tests that share the same callback.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<script>
+// generate_tests takes an array of arrays that define tests
+// but lets pass it an empty array and verify it does nothing.
+function null_callback() {
+ throw "null_callback should not be called.";
+}
+generate_tests(null_callback, []);
+
+// Generate 3 tests specifying the name and one parameter
+function validate_arguments(arg1) {
+ assert_equals(arg1, 1, "Ensure that we get our expected argument");
+}
+generate_tests(validate_arguments, [
+ ["first test", 1],
+ ["second test", 1],
+ ["third test", 1],
+]);
+
+// Generate a test passing in a properties object that is shared across tests.
+function validate_properties() {
+ assert_true(this.properties.sentinel, "Ensure that we got the right properties object.");
+}
+generate_tests(validate_properties, [["sentinel check 1"], ["sentinel check 2"]], {sentinel: true});
+
+// Generate a test passing in a properties object that is shared across tests.
+function validate_separate_properties() {
+ if (this.name === "sentinel check 1 unique properties") {
+ assert_true(this.properties.sentinel, "Ensure that we got the right properties object. Expect sentinel: true.");
+ }
+ else {
+ assert_false(this.properties.sentinel, "Ensure that we got the right properties object. Expect sentinel: false.");
+ }
+}
+generate_tests(validate_separate_properties, [["sentinel check 1 unique properties"], ["sentinel check 2 unique properties"]], [{sentinel: true}, {sentinel: false}]);
+
+// Finally generate a complicated set of tests from another data source
+var letters = ["a", "b", "c", "d", "e", "f"];
+var numbers = [0, 1, 2, 3, 4, 5];
+function validate_related_arguments(arg1, arg2) {
+ assert_equals(arg1.charCodeAt(0) - "a".charCodeAt(0), arg2, "Ensure that we can map letters to numbers.");
+}
+function format_as_test(letter, index, letters) {
+ return ["Test to map " + letter + " to " + numbers[index], letter, numbers[index]];
+}
+generate_tests(validate_related_arguments, letters.map(format_as_test));
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Test to map a to 0",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test to map b to 1",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test to map c to 2",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test to map d to 3",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test to map e to 4",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test to map f to 5",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "first test",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "second test",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "sentinel check 1",
+ "properties": {
+ "sentinel": true
+ },
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "sentinel check 1 unique properties",
+ "properties": {
+ "sentinel": true
+ },
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "sentinel check 2",
+ "properties": {
+ "sentinel": true
+ },
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "sentinel check 2 unique properties",
+ "properties": {
+ "sentinel": false
+ },
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "third test",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlDictionary/test_partial_interface_of.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlDictionary/test_partial_interface_of.html
new file mode 100644
index 0000000000..05e6e0b1e0
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlDictionary/test_partial_interface_of.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <meta name="variant" content="">
+ <title>idlharness: Partial dictionary</title>
+ <script src="/resources/test/variants.js"></script>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+</head>
+
+<body>
+ <p>Verify the series of sub-tests that are executed for "partial" dictionary objects.</p>
+ <script>
+ "use strict";
+
+ // No original existence
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls('partial dictionary A {};');
+ idlArray.test();
+ })();
+
+ // Multiple partials existence
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_untested_idls('partial dictionary B {};');
+ idlArray.add_idls('partial dictionary B {};');
+ idlArray.add_idls('partial dictionary B {};');
+ idlArray.add_idls('partial dictionary B {};');
+ idlArray.add_idls('dictionary B {};');
+ idlArray.test();
+ })();
+
+ // Original is a namespace, not a dictionary.
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls(`
+ partial dictionary C {};
+ namespace C {};`);
+ idlArray.merge_partials();
+ })();
+ </script>
+ <script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "name": "Partial dictionary A: original dictionary defined",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: Original dictionary should be defined expected true got false"
+ },
+ {
+ "name": "Partial dictionary B[2]: original dictionary defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial dictionary B[3]: original dictionary defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial dictionary B[4]: original dictionary defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial dictionary C: original dictionary defined",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: Original C definition should have type dictionary expected true got false"
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_exposed_wildcard.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_exposed_wildcard.html
new file mode 100644
index 0000000000..addc0eb4fc
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_exposed_wildcard.html
@@ -0,0 +1,233 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>idlharness: Exposed=*</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+</head>
+<body>
+<script>
+"use strict";
+
+Object.defineProperty(window, "A", {
+ enumerable: false,
+ writable: true,
+ configurable: true,
+ value: function A() {}
+ });
+Object.defineProperty(window.A, "prototype", {
+ writable: false,
+ value: window.A.prototype
+ });
+A.prototype[Symbol.toStringTag] = "A";
+
+Object.defineProperty(window, "C", {
+ enumerable: false,
+ writable: true,
+ configurable: true,
+ value: function C() {}
+ });
+Object.defineProperty(window.C, "prototype", {
+ writable: false,
+ value: window.C.prototype
+ });
+C.prototype[Symbol.toStringTag] = "C";
+
+Object.defineProperty(window, "D", {
+ enumerable: false,
+ writable: true,
+ configurable: true,
+ value: function D() {}
+ });
+Object.defineProperty(window.D, "prototype", {
+ writable: false,
+ value: window.D.prototype
+ });
+C.prototype[Symbol.toStringTag] = "D";
+Object.defineProperty(window, "B", {
+ enumerable: false,
+ writable: true,
+ configurable: true,
+ value: window.A
+ });
+
+var idlArray = new IdlArray();
+idlArray.add_idls(`
+[Exposed=*, LegacyWindowAlias=B] interface A {};
+[Exposed=*] partial interface A {};
+[Exposed=Window] interface C {};
+[Exposed=*] partial interface C {};
+[Exposed=*] interface D {};
+[Exposed=Window] partial interface D {};
+`);
+idlArray.add_objects({
+ Window: ["window"]
+});
+idlArray.test();
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "name": "A interface object length",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "A interface object name",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "A interface: existence and properties of interface object",
+ "properties": {},
+ "message": "assert_throws_js: interface object didn't throw TypeError when called as a function function \"function() {\n interface_object();\n }\" did not throw",
+ "status_string": "FAIL"
+ },
+ {
+ "name": "A interface: existence and properties of interface prototype object",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "A interface: existence and properties of interface prototype object's \"constructor\" property",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "A interface: existence and properties of interface prototype object's @@unscopables property",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "A interface: legacy window alias",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "C interface object length",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "C interface object name",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "C interface: existence and properties of interface object",
+ "properties": {},
+ "message": "assert_throws_js: interface object didn't throw TypeError when called as a function function \"function() {\n interface_object();\n }\" did not throw",
+ "status_string": "FAIL"
+ },
+ {
+ "name": "C interface: existence and properties of interface prototype object",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "C interface: existence and properties of interface prototype object's \"constructor\" property",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "C interface: existence and properties of interface prototype object's @@unscopables property",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "D interface object length",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "D interface object name",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "D interface: existence and properties of interface object",
+ "properties": {},
+ "message": "assert_throws_js: interface object didn't throw TypeError when called as a function function \"function() {\n interface_object();\n }\" did not throw",
+ "status_string": "FAIL"
+ },
+ {
+ "name": "D interface: existence and properties of interface prototype object",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "D interface: existence and properties of interface prototype object's \"constructor\" property",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "D interface: existence and properties of interface prototype object's @@unscopables property",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "Partial interface A: original interface defined",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "Partial interface A: valid exposure set",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "Partial interface C: original interface defined",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "Partial interface C: valid exposure set",
+ "properties": {},
+ "message": "Partial C interface is exposed everywhere, the original interface is not.",
+ "status_string": "FAIL"
+ },
+ {
+ "name": "Partial interface D: original interface defined",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ },
+ {
+ "name": "Partial interface D: valid exposure set",
+ "properties": {},
+ "message": null,
+ "status_string": "PASS"
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_immutable_prototype.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_immutable_prototype.html
new file mode 100644
index 0000000000..5fe05915b0
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_immutable_prototype.html
@@ -0,0 +1,298 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>idlharness: Immutable prototypes</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+</head>
+<body>
+<script>
+"use strict";
+
+Object.defineProperty(window, "Foo", {
+ enumerable: false,
+ writable: true,
+ configurable: true,
+ value: function Foo() {
+ if (!new.target) {
+ throw new TypeError('Foo() must be called with new');
+ }
+ }
+ });
+Object.defineProperty(window.Foo, "prototype", {
+ writable: false,
+ value: window.Foo.prototype
+ });
+Foo.prototype[Symbol.toStringTag] = "Foo";
+
+var idlArray = new IdlArray();
+idlArray.add_untested_idls("interface EventTarget {};");
+idlArray.add_idls(
+ "[Global=Window, Exposed=Window]\n" +
+ "interface Window : EventTarget {};\n" +
+
+ "[Global=Window, Exposed=Window]\n" +
+ "interface Foo { constructor(); };"
+ );
+idlArray.add_objects({
+ Foo: ["new Foo()"],
+ Window: ["window"]
+});
+idlArray.test();
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "name": "Foo interface object length",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface object name",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: existence and properties of interface object",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: existence and properties of interface prototype object",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: existence and properties of interface prototype object's \"constructor\" property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: existence and properties of interface prototype object's @@unscopables property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via Object.setPrototypeOf should throw a TypeError",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_throws_js: function \"function() {\n Object.setPrototypeOf(obj, newValue);\n }\" did not throw"
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via Reflect.setPrototypeOf should return false",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_false: expected false got true"
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via __proto__ should throw a TypeError",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_throws_js: function \"function() {\n obj.__proto__ = newValue;\n }\" did not throw"
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via Object.setPrototypeOf should not throw",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via Reflect.setPrototypeOf should return true",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via __proto__ should not throw",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via Object.setPrototypeOf should throw a TypeError",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_throws_js: function \"function() {\n Object.setPrototypeOf(obj, newValue);\n }\" did not throw"
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via Reflect.setPrototypeOf should return false",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_false: expected false got true"
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via __proto__ should throw a TypeError",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_throws_js: function \"function() {\n obj.__proto__ = newValue;\n }\" did not throw"
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via Object.setPrototypeOf should not throw",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via Reflect.setPrototypeOf should return true",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via __proto__ should not throw",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo must be primary interface of new Foo()",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Stringification of new Foo()",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Stringification of window",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface object length",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface object name",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: existence and properties of interface object",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: existence and properties of interface prototype object",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: existence and properties of interface prototype object's \"constructor\" property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: existence and properties of interface prototype object's @@unscopables property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via Object.setPrototypeOf should throw a TypeError",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via Reflect.setPrototypeOf should return false",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via __proto__ should throw a TypeError",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via Object.setPrototypeOf should not throw",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via Reflect.setPrototypeOf should return true",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via __proto__ should not throw",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via Object.setPrototypeOf should throw a TypeError",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via Reflect.setPrototypeOf should return false",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via __proto__ should throw a TypeError",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via Object.setPrototypeOf should not throw",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via Reflect.setPrototypeOf should return true",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via __proto__ should not throw",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Window must be primary interface of window",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_interface_mixin.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_interface_mixin.html
new file mode 100644
index 0000000000..be2844e698
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_interface_mixin.html
@@ -0,0 +1,131 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>idlharness: interface mixins</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+</head>
+
+<body>
+ <p>Verify the series of sub-tests that are executed for "interface mixin" objects.</p>
+ <script>
+ "use strict";
+
+ // Simple includes statement (valid)
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls(`
+ [Exposed=Window] interface I1 {};
+ interface mixin M1 { attribute any a1; };
+ I1 includes M1;`);
+ idlArray.merge_partials();
+ idlArray.merge_mixins();
+ })();
+
+ // Partial interface mixin (valid)
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls(`
+ [Exposed=Window] interface I2 {};
+ interface mixin M2 {};
+ partial interface mixin M2 { attribute any a2; };
+ I2 includes M2;`);
+ idlArray.merge_partials();
+ idlArray.merge_mixins();
+ })();
+
+ // Partial interface mixin without original mixin
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls('partial interface mixin M3 {};');
+ idlArray.merge_partials();
+ idlArray.merge_mixins();
+ })();
+
+ // Name clash between mixin and partial mixin
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls(`
+ interface mixin M4 { attribute any a4; };
+ partial interface mixin M4 { attribute any a4; };`);
+ idlArray.merge_partials();
+ idlArray.merge_mixins();
+ })();
+
+ // Name clash between interface and mixin
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_untested_idls(`
+ interface mixin M5 { attribute any a5; };
+ interface I5 { attribute any a5; };
+ I5 includes M5;`);
+ idlArray.merge_partials();
+ idlArray.merge_mixins();
+ })();
+ </script>
+ <script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "name": "I1 includes M1: member names are unique",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "I2 includes M2: member names are unique",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "I5 includes M5: member names are unique",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: member a5 is unique expected true got false"
+ },
+ {
+ "name": "Partial interface mixin M2: member names are unique",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial interface mixin M2: original interface mixin defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial interface mixin M3: original interface mixin defined",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: Original interface mixin should be defined expected true got false"
+ },
+ {
+ "name": "Partial interface mixin M4: member names are unique",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: member a4 is unique expected true got false"
+ },
+ {
+ "name": "Partial interface mixin M4: original interface mixin defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_partial_interface_of.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_partial_interface_of.html
new file mode 100644
index 0000000000..671196cc5d
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_partial_interface_of.html
@@ -0,0 +1,188 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <meta name="variant" content="">
+ <title>idlharness: Partial interface</title>
+ <script src="/resources/test/variants.js"></script>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+</head>
+
+<body>
+ <p>Verify the series of sub-tests that are executed for "partial" interface objects.</p>
+ <script>
+ "use strict";
+
+ // No original existence
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls('partial interface A {};');
+ idlArray.test();
+ })();
+
+ // Valid exposure (Note: Worker -> {Shared,Dedicated,Service}Worker)
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_untested_idls(`
+ [Exposed=(Worker)]
+ interface B {};
+
+ [Exposed=(DedicatedWorker, ServiceWorker, SharedWorker)]
+ interface C {};`);
+ idlArray.add_idls(`
+ [Exposed=(DedicatedWorker, ServiceWorker, SharedWorker)]
+ partial interface B {};
+
+ [Exposed=(Worker)]
+ partial interface C {};`);
+ idlArray.merge_partials();
+ })();
+
+ // Invalid exposure
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_untested_idls(`
+ [Exposed=(Window, ServiceWorker)]
+ interface D {};`);
+ idlArray.add_idls(`
+ [Exposed=(DedicatedWorker)]
+ partial interface D {};`);
+ idlArray.merge_partials();
+ })();
+
+ // Original is a namespace, not an interface.
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls(`
+ partial interface E {};
+ namespace E {};`);
+ idlArray.merge_partials();
+ })();
+
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls(`
+ partial interface F {};
+ partial interface mixin G {};
+ `);
+ idlArray.add_dependency_idls(`
+ interface F {};
+ interface mixin G {};
+ interface mixin H {};
+ F includes H;
+ I includes H;
+ J includes G;
+ interface K : J {};
+ interface L : F {};
+ `);
+ test(() => {
+ // Convert idlArray.includes into a Map from name of target interface to
+ // name of included mixin. (This assumes each interface includes at most
+ // one mixin, otherwise later includes would clobber earlier ones.)
+ const includes = new Map(idlArray.includes.map(i => [i.target, i.includes]));
+ // F is tested, so H is a dep.
+ assert_equals(includes.get('F'), 'H', 'F should be picked up');
+ // H is not tested, so I is not a dep.
+ assert_false(includes.has('I'), 'I should be ignored');
+ // G is a dep, so J is a dep.
+ assert_equals(includes.get('J'), 'G', 'J should be picked up');
+ // K isn't a dep because J isn't defined.
+ assert_false('K' in idlArray.members, 'K should be ignored');
+ // L isn't a dep because F is untested.
+ assert_false('L' in idlArray.members, 'L should be ignored');
+ }, 'partial mixin dep implications');
+ })();
+
+ // Name clash (partials)
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls(`
+ interface M { attribute any A; };
+ partial interface M { attribute any A; };`);
+ idlArray.merge_partials();
+ })();
+ </script>
+ <script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "name": "Partial interface A: original interface defined",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: Original interface should be defined expected true got false"
+ },
+ {
+ "name": "Partial interface B: original interface defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial interface B: valid exposure set",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial interface C: original interface defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial interface C: valid exposure set",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial interface D: original interface defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial interface D: valid exposure set",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "Partial D interface is exposed to 'DedicatedWorker', the original interface is not."
+ },
+ {
+ "name": "Partial interface E: original interface defined",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: Original E definition should have type interface expected true got false"
+ },
+ {
+ "name": "partial mixin dep implications",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial interface M: original interface defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial interface M: member names are unique",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: member A is unique expected true got false"
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_primary_interface_of.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_primary_interface_of.html
new file mode 100644
index 0000000000..309de60bb7
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_primary_interface_of.html
@@ -0,0 +1,116 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>idlharness: Primary interface</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+</head>
+<body>
+<p>Verify the series of sub-tests that are executed for "tested" interface
+objects but skipped for "untested" interface objects.</p>
+<script>
+"use strict";
+
+function FooParent() {
+ if (!new.target) {
+ throw new TypeError('FooParent() must be called with new');
+ }
+}
+Object.defineProperty(window, "Foo", {
+ enumerable: false,
+ writable: true,
+ configurable: true,
+ value: function Foo() {
+ if (!new.target) {
+ throw new TypeError('Foo() must be called with new');
+ }
+ }
+ });
+Object.defineProperty(window.Foo, "prototype", {
+ writable: false,
+ value: new FooParent()
+ });
+Object.defineProperty(window.Foo.prototype, "constructor", {
+ enumerable: false,
+ writable: true,
+ configurable: true,
+ value: window.Foo
+ });
+Object.setPrototypeOf(Foo, FooParent);
+Foo.prototype[Symbol.toStringTag] = "Foo";
+
+var idlArray = new IdlArray();
+idlArray.add_untested_idls("interface FooParent {};");
+idlArray.add_idls(
+ "interface Foo : FooParent { constructor(); };"
+ );
+idlArray.add_objects({
+ Foo: ["new Foo()"],
+ FooParent: ["new FooParent()"]
+});
+idlArray.test();
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "name": "Foo interface object length",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface object name",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: existence and properties of interface object",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: existence and properties of interface prototype object",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: existence and properties of interface prototype object's \"constructor\" property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo interface: existence and properties of interface prototype object's @@unscopables property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Foo must be primary interface of new Foo()",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Stringification of new Foo()",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_to_json_operation.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_to_json_operation.html
new file mode 100644
index 0000000000..bbc502a313
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_to_json_operation.html
@@ -0,0 +1,177 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>IdlInterface.prototype.test_to_json_operation()</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+ <script src="../../../../idl-helper.js"></script>
+</head>
+<body>
+<script>
+ "use strict";
+ function wrap(member, obj) {
+ function F(obj) {
+ this._obj = obj;
+ }
+
+ F.prototype.toJSON = function() {
+ return this._obj;
+ }
+ Object.defineProperty(F, 'name', { value: member.name });
+ return new F(obj);
+ }
+
+ var i, obj;
+ i = interfaceFrom("interface A { [Default] object toJSON(); attribute long foo; };");
+ i.test_to_json_operation("object", wrap(i, { foo: 123 }), i.members[0]);
+
+ // should fail (wrong type)
+ i = interfaceFrom("interface B { [Default] object toJSON(); attribute long foo; };");
+ i.test_to_json_operation("object", wrap(i, { foo: "a value" }), i.members[0]);
+
+ // should handle extraneous attributes (e.g. from an extension specification)
+ i = interfaceFrom("interface C { [Default] object toJSON(); attribute long foo; };");
+ i.test_to_json_operation("object", wrap(i, { foo: 123, bar: 456 }), i.members[0]);
+
+ // should fail (missing property)
+ i = interfaceFrom("interface D { [Default] object toJSON(); attribute long foo; };");
+ i.test_to_json_operation("object", wrap(i, { }), i.members[0]);
+
+ // should fail (should be writable)
+ obj = Object.defineProperties({}, { foo: {
+ writable: false,
+ enumerable: true,
+ configurable: true,
+ value: 123
+ }});
+ i = interfaceFrom("interface F { [Default] object toJSON(); attribute long foo; };");
+ i.test_to_json_operation("object", wrap(i, obj), i.members[0]);
+
+ // should fail (should be enumerable)
+ obj = Object.defineProperties({}, { foo: {
+ writable: true,
+ enumerable: false,
+ configurable: true,
+ value: 123
+ }});
+ i = interfaceFrom("interface G { [Default] object toJSON(); attribute long foo; };");
+ i.test_to_json_operation("object", wrap(i, obj), i.members[0]);
+
+ // should fail (should be configurable)
+ obj = Object.defineProperties({}, { foo: {
+ writable: true,
+ enumerable: true,
+ configurable: false,
+ value: 123
+ }});
+ i = interfaceFrom("interface H { [Default] object toJSON(); attribute long foo; };");
+ i.test_to_json_operation("object", wrap(i, obj), i.members[0]);
+
+ var idl = new IdlArray();
+ idl.add_idls("interface I : J { [Default] object toJSON(); attribute long foo; };");
+ idl.add_idls("interface J { [Default] object toJSON(); attribute DOMString foo;};");
+ var i = idl.members.I;
+ i.test_to_json_operation("object", wrap(i, { foo: 123 }), i.members[0]);
+
+ i = interfaceFrom("interface K { [Default] object toJSON(); };");
+ i.test_to_json_operation("object", wrap(i, {}), i.members[0]);
+
+ i = interfaceFrom("interface L { DOMString toJSON(); };");
+ i.test_to_json_operation("object", wrap(i, "a string"), i.members[0]);
+
+ // should fail (wrong output type)
+ i = interfaceFrom("interface M { DOMString toJSON(); };");
+ i.test_to_json_operation("object", wrap(i, {}), i.members[0]);
+
+ // should fail (not an IDL type)
+ i = interfaceFrom("interface N { DOMException toJSON(); };");
+ i.test_to_json_operation("object", wrap(i, {}), i.members[0]);
+</script>
+<script type="text/json" id="expected">
+ {
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "message": null,
+ "name": "A interface: default toJSON operation on object",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": "assert_equals: expected \"number\" but got \"string\"",
+ "name": "B interface: default toJSON operation on object",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": null,
+ "name": "C interface: default toJSON operation on object",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": "assert_true: property \"foo\" should be present in the output of D.prototype.toJSON() expected true got false",
+ "name": "D interface: default toJSON operation on object",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": "assert_true: property foo should be writable expected true got false",
+ "name": "F interface: default toJSON operation on object",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": "assert_true: property foo should be enumerable expected true got false",
+ "name": "G interface: default toJSON operation on object",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": "assert_true: property foo should be configurable expected true got false",
+ "name": "H interface: default toJSON operation on object",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": null,
+ "name": "I interface: default toJSON operation on object",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "K interface: default toJSON operation on object",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "L interface: toJSON operation on object",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": "assert_equals: expected \"string\" but got \"object\"",
+ "name": "M interface: toJSON operation on object",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": "assert_true: {\"type\":\"return-type\",\"extAttrs\":[],\"generic\":\"\",\"nullable\":false,\"union\":false,\"idlType\":\"DOMException\"} is not an appropriate return value for the toJSON operation of N expected true got false",
+ "name": "N interface: toJSON operation on object",
+ "properties": {},
+ "status_string": "FAIL"
+ }
+ ],
+ "type": "complete"
+ }
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_attribute.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_attribute.html
new file mode 100644
index 0000000000..2c94061fc1
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_attribute.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>idlharness: namespace attribute</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+</head>
+<body>
+<p>Verify the series of sub-tests that are executed for namespace attributes.</p>
+<script>
+"use strict";
+
+Object.defineProperty(self, "foo", {
+ value: {
+ truth: true,
+ },
+ writable: true,
+ enumerable: false,
+ configurable: true,
+});
+
+var idlArray = new IdlArray();
+idlArray.add_idls(`
+[Exposed=Window]
+namespace foo {
+ readonly attribute bool truth;
+ readonly attribute bool lies;
+};`);
+idlArray.test();
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "name": "foo namespace: extended attributes",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: property descriptor",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: [[Extensible]] is true",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: [[Prototype]] is Object.prototype",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: typeof is \"object\"",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: has no length property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: has no name property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: attribute truth",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: attribute lies",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_own_property: foo does not have property \"lies\" expected property \"lies\" missing"
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_operation.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_operation.html
new file mode 100644
index 0000000000..da70c8fa31
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_operation.html
@@ -0,0 +1,242 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>idlharness: namespace operation</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+</head>
+<body>
+<p>Verify the series of sub-tests that are executed for namespace operations.</p>
+<script>
+"use strict";
+
+Object.defineProperty(self, "foo", {
+ value: Object.defineProperty({}, "Truth", {
+ value: function() {},
+ writable: true,
+ enumerable: true,
+ configurable: true,
+ }),
+ writable: true,
+ enumerable: false,
+ configurable: true,
+});
+
+Object.defineProperty(self, "bar", {
+ value: Object.defineProperty({}, "Truth", {
+ value: function() {},
+ writable: false,
+ enumerable: true,
+ configurable: false,
+ }),
+ writable: true,
+ enumerable: false,
+ configurable: true,
+});
+
+Object.defineProperty(self, "baz", {
+ value: {
+ LongStory: function(hero, ...details) {
+ return `${hero} went and ${details.join(', then')}`
+ },
+ ShortStory: function(...details) {
+ return `${details.join('. ')}`;
+ },
+ },
+ writable: true,
+ enumerable: false,
+ configurable: true,
+});
+
+var idlArray = new IdlArray();
+idlArray.add_idls(`
+[Exposed=Window]
+namespace foo {
+ undefined Truth();
+ undefined Lies();
+};
+[Exposed=Window]
+namespace bar {
+ [LegacyUnforgeable]
+ undefined Truth();
+};
+[Exposed=Window]
+namespace baz {
+ DOMString LongStory(any hero, DOMString... details);
+ DOMString ShortStory(DOMString... details);
+};`);
+idlArray.test();
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "name": "foo namespace: extended attributes",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: property descriptor",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: [[Extensible]] is true",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: [[Prototype]] is Object.prototype",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: typeof is \"object\"",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: has no length property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: has no name property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: operation Truth()",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "foo namespace: operation Lies()",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_own_property: namespace object missing operation \"Lies\" expected property \"Lies\" missing"
+ },
+ {
+ "name": "bar namespace: extended attributes",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "bar namespace: property descriptor",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "bar namespace: [[Extensible]] is true",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "bar namespace: [[Prototype]] is Object.prototype",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "bar namespace: typeof is \"object\"",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "bar namespace: has no length property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "bar namespace: has no name property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "bar namespace: operation Truth()",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+
+ {
+ "name": "baz namespace: extended attributes",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "baz namespace: property descriptor",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "baz namespace: [[Extensible]] is true",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "baz namespace: [[Prototype]] is Object.prototype",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "baz namespace: typeof is \"object\"",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "baz namespace: has no length property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "baz namespace: has no name property",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "baz namespace: operation LongStory(any, DOMString...)",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "baz namespace: operation ShortStory(DOMString...)",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_partial_namespace.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_partial_namespace.html
new file mode 100644
index 0000000000..eabdcd10a9
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_partial_namespace.html
@@ -0,0 +1,125 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>idlharness: Partial namespace</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+</head>
+
+<body>
+ <p>Verify the series of sub-tests that are executed for "partial" namespace objects.</p>
+ <script>
+ "use strict";
+
+ // No original existence
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls('partial namespace A {};');
+ idlArray.test();
+ })();
+
+ // Valid exposure (Note: Worker -> {Shared,Dedicated,Service}Worker)
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_untested_idls(`
+ [Exposed=(Worker)]
+ namespace B {};
+
+ [Exposed=(DedicatedWorker, ServiceWorker, SharedWorker)]
+ namespace C {};`);
+ idlArray.add_idls(`
+ [Exposed=(DedicatedWorker, ServiceWorker, SharedWorker)]
+ partial namespace B {};
+
+ [Exposed=(Worker)]
+ partial namespace C {};`);
+ idlArray.merge_partials();
+ })();
+
+ // Invalid exposure
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_untested_idls(`
+ [Exposed=(Window, ServiceWorker)]
+ namespace D {};`);
+ idlArray.add_idls(`
+ [Exposed=(DedicatedWorker)]
+ partial namespace D {};`);
+ idlArray.merge_partials();
+ })();
+
+ // Original is an interface, not a namespace.
+ (() => {
+ const idlArray = new IdlArray();
+ idlArray.add_idls(`
+ partial namespace E {};
+ interface E {};`);
+ idlArray.merge_partials();
+ })();
+ </script>
+ <script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "name": "Partial namespace A: original namespace defined",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: Original namespace should be defined expected true got false"
+ },
+ {
+ "name": "Partial namespace B: original namespace defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial namespace B: valid exposure set",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial namespace C: original namespace defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial namespace C: valid exposure set",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial namespace D: original namespace defined",
+ "status_string": "PASS",
+ "properties": {},
+ "message": null
+ },
+ {
+ "name": "Partial namespace D: valid exposure set",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "Partial D namespace is exposed to 'DedicatedWorker', the original namespace is not."
+ },
+ {
+ "name": "Partial namespace E: original namespace defined",
+ "status_string": "FAIL",
+ "properties": {},
+ "message": "assert_true: Original E definition should have type namespace expected true got false"
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/iframe-callback.html b/testing/web-platform/tests/resources/test/tests/functional/iframe-callback.html
new file mode 100644
index 0000000000..f49d0aa6b8
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/iframe-callback.html
@@ -0,0 +1,116 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Example with iframe that notifies containing document via callbacks</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body onload="start_test_in_iframe()">
+<h1>Callbacks From Tests Running In An IFRAME</h1>
+<p>A test is run inside an <tt>iframe</tt> with a same origin document. The
+containing document should receive callbacks as the tests progress inside the
+<tt>iframe</tt>. A single passing test is expected in the summary below.
+<div id="log"></div>
+
+<script>
+var callbacks = [];
+var START = 1
+var TEST_STATE = 2
+var RESULT = 3
+var COMPLETION = 4
+var test_complete = false;
+
+setup({explicit_done: true});
+
+// The following callbacks are called for tests in this document as well as the
+// tests in the IFRAME. Currently, callbacks invoked from this document and any
+// child document are indistinguishable from each other.
+
+function start_callback(properties) {
+ callbacks.push(START);
+}
+
+function test_state_callback(test) {
+ callbacks.push(TEST_STATE);
+}
+
+function result_callback(test) {
+ callbacks.push(RESULT);
+}
+
+function completion_callback(tests, status) {
+ if (test_complete) {
+ return;
+ }
+ test_complete = true;
+ callbacks.push(COMPLETION);
+ verify_received_callbacks();
+ done();
+}
+
+function verify_received_callbacks() {
+ var copy_of_callbacks = callbacks.slice(0);
+
+ // Note that you can't run test assertions directly in a callback even if
+ // this is a file test. When the callback is invoked from a same-origin child
+ // page, the callstack reaches into the calling child document. Any
+ // exception thrown in a callback will be handled by the child rather than
+ // this document.
+ test(
+ function() {
+ // callbacks list should look like:
+ // START 1*(TEST_STATE) RESULT COMPLETION
+ assert_equals(copy_of_callbacks.shift(), START,
+ "The first received callback should be 'start_callback'.");
+ assert_equals(copy_of_callbacks.shift(), TEST_STATE,
+ "'test_state_callback' should be received before any " +
+ "result or completion callbacks.");
+ while(copy_of_callbacks.length > 0) {
+ var callback = copy_of_callbacks.shift();
+ if (callback != TEST_STATE) {
+ copy_of_callbacks.unshift(callback);
+ break;
+ }
+ }
+ assert_equals(copy_of_callbacks.shift(), RESULT,
+ "'test_state_callback' should be followed by 'result_callback'.");
+ assert_equals(copy_of_callbacks.shift(), COMPLETION,
+ "Final 'result_callback' should be followed by 'completion_callback'.");
+ assert_equals(copy_of_callbacks.length, 0,
+ "'completion_callback' should be the last callback.");
+ });
+}
+
+function start_test_in_iframe() {
+ // This document is going to clear any received callbacks and maintain
+ // radio silence until the test in the iframe runs to completion. The
+ // completion_callback() will then complete the testing on this document.
+ callbacks.length = 0;
+ var iframe = document.createElement("iframe");
+ // single-page-test-pass.html has a single test.
+ iframe.src = "single-page-test-pass.html";
+ iframe.style.setProperty("display", "none");
+ document.getElementById("target").appendChild(iframe);
+}
+</script>
+
+<div id="target">
+</div>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Example with iframe that notifies containing document via callbacks",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/iframe-consolidate-errors.html b/testing/web-platform/tests/resources/test/tests/functional/iframe-consolidate-errors.html
new file mode 100644
index 0000000000..ef9b8702ec
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/iframe-consolidate-errors.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Example with iframe that consolidates errors via fetch_tests_from_window</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+var parent_test = async_test("Test executing in parent context");
+</script>
+</head>
+<body onload="parent_test.done()">
+<h1>Fetching Tests From a Child Context</h1>
+<p>This test demonstrates the use of <tt>fetch_tests_from_window</tt> to pull
+tests from an <tt>iframe</tt> into the primary document.</p>
+<p>The test suite is expected to fail due to an unhandled exception in the
+child context.</p>
+<div id="log"></div>
+
+<iframe id="childContext" src="uncaught-exception-handle.html" style="display:none"></iframe>
+<!-- apisample4.html is a failing suite due to an unhandled Error. -->
+
+<script>
+ var childContext = document.getElementById("childContext");
+ fetch_tests_from_window(childContext.contentWindow);
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Error in remote: Error: Example Error"
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Test executing in parent context",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "This should show a harness status of 'Error' and a test status of 'Not Run'",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/iframe-consolidate-tests.html b/testing/web-platform/tests/resources/test/tests/functional/iframe-consolidate-tests.html
new file mode 100644
index 0000000000..246dddee11
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/iframe-consolidate-tests.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Example with iframe that consolidates tests via fetch_tests_from_window</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+var parent_test = async_test("Test executing in parent context");
+</script>
+</head>
+<body onload="parent_test.done()">
+<h1>Fetching Tests From a Child Context</h1>
+<p>This test demonstrates the use of <tt>fetch_tests_from_window</tt> to pull
+tests from an <tt>iframe</tt> into the primary document.</p>
+<p>The test suite will not complete until tests in the child context have finished
+executing</p>
+<div id="log"></div>
+
+<iframe id="childContext" src="promise-async.html" style="display:none"></iframe>
+
+<script>
+ var childContext = document.getElementById("childContext");
+ fetch_tests_from_window(childContext.contentWindow);
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Promise rejection",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Promise resolution",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Promises and test assertion failures (should fail)",
+ "properties": {},
+ "message": "assert_true: This failure is expected expected true got false"
+ },
+ {
+ "status_string": "PASS",
+ "name": "Promises are supported in your browser",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Promises resolution chaining",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test executing in parent context",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Use of step_func with Promises",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Use of unreached_func with Promises (should fail)",
+ "properties": {},
+ "message": "assert_unreached: This failure is expected Reached unreachable code"
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/iframe-msg.html b/testing/web-platform/tests/resources/test/tests/functional/iframe-msg.html
new file mode 100644
index 0000000000..283a5d98cc
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/iframe-msg.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Example with iframe that notifies containing document via cross document messaging</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<h1>Notifications From Tests Running In An IFRAME</h1>
+<p>A test is run inside an <tt>iframe</tt> with a same origin document. The
+containing document should receive messages via <tt>postMessage</tt>/
+<tt>onmessage</tt> as the tests progress inside the <tt>iframe</tt>. A single
+passing test is expected in the summary below.
+</p>
+<div id="log"></div>
+
+<script>
+var t = async_test("Containing document receives messages");
+var start_received = false;
+var result_received = false;
+var completion_received = false;
+
+// These are the messages that are expected to be seen while running the tests
+// in the IFRAME.
+var expected_messages = [
+ t.step_func(
+ function(message) {
+ assert_equals(message.data.type, "start");
+ assert_own_property(message.data, "properties");
+ }),
+
+ t.step_func(
+ function(message) {
+ assert_equals(message.data.type, "test_state");
+ assert_equals(message.data.test.status, message.data.test.NOTRUN);
+ }),
+
+ t.step_func(
+ function(message) {
+ assert_equals(message.data.type, "result");
+ assert_equals(message.data.test.status, message.data.test.PASS);
+ }),
+
+ t.step_func(
+ function(message) {
+ assert_equals(message.data.type, "complete");
+ assert_equals(message.data.tests.length, 1);
+ assert_equals(message.data.tests[0].status,
+ message.data.tests[0].PASS);
+ assert_equals(message.data.status.status, message.data.status.OK);
+ t.done();
+ }),
+
+ t.unreached_func("Too many messages received")
+];
+
+on_event(window,
+ "message",
+ function(message) {
+ var handler = expected_messages.shift();
+ handler(message);
+ });
+</script>
+<iframe src="single-page-test-pass.html" style="display:none">
+ <!-- single-page-test-pass.html implements a file_is_test test. -->
+</iframe>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Containing document receives messages",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/log-insertion.html b/testing/web-platform/tests/resources/test/tests/functional/log-insertion.html
new file mode 100644
index 0000000000..9a63c3dbde
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/log-insertion.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<title>Log insertion</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(function(t) {
+ assert_equals(document.body, null);
+}, "Log insertion before load");
+test(function(t) {
+ assert_equals(document.body, null);
+}, "Log insertion before load (again)");
+async_test(function(t) {
+ window.onload = t.step_func_done(function() {
+ var body = document.body;
+ assert_not_equals(body, null);
+
+ var log = document.getElementById("log");
+ assert_equals(log.parentNode, body);
+ });
+}, "Log insertion after load");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [{
+ "status_string": "PASS",
+ "name": "Log insertion before load",
+ "message": null,
+ "properties": {}
+ }, {
+ "status_string": "PASS",
+ "name": "Log insertion before load (again)",
+ "message": null,
+ "properties": {}
+ }, {
+ "status_string": "PASS",
+ "name": "Log insertion after load",
+ "message": null,
+ "properties": {}
+ }],
+ "type": "complete"
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/no-title.html b/testing/web-platform/tests/resources/test/tests/functional/no-title.html
new file mode 100644
index 0000000000..a337e4e5f5
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/no-title.html
@@ -0,0 +1,146 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Tests with no title</title>
+</head>
+<script src="/resources/testharness.js"></script>
+
+<body>
+<h1>Tests with no title</h1>
+<div id="log"></div>
+<script>
+ test(function(){assert_true(true, '1')});
+ test(()=>assert_true(true, '2'));
+ test(() => assert_true(true, '3'));
+ test(() => assert_true(true, '3')); // test duplicate behaviour
+ test(() => assert_true(true, '3')); // test duplicate behaviour
+ test(() => {
+ assert_true(true, '4');
+ });
+ test(() => { assert_true(true, '5') });
+ test(() => { assert_true(true, '6') } );
+ test(() => { assert_true(true, '7'); });
+ test(() => {});
+ test(() => { });
+ test(() => {;;;;});
+ test(() => { ; ; ; ; });
+ test(()=>{});
+ test(()=>{ });
+ test(()=>{;;;;});
+ test(()=>{ ; ; ; ; });
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_true(true, '2')",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_true(true, '3')",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_true(true, '3') 1",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_true(true, '3') 2",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title 1",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_true(true, '5')",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_true(true, '6')",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "assert_true(true, '7')",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title 2",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title 3",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title 4",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title 5",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title 6",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title 7",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title 8",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Tests with no title 9",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/order.html b/testing/web-platform/tests/resources/test/tests/functional/order.html
new file mode 100644
index 0000000000..686383861a
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/order.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Ordering</title>
+<meta name="timeout" content="6000">
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(function() {}, 'second');
+test(function() {}, 'first');
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [{
+ "status_string": "PASS",
+ "name": "first",
+ "message": null,
+ "properties": {}
+ }, {
+ "status_string": "PASS",
+ "name": "second",
+ "message": null,
+ "properties": {}
+ }],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/promise-async.html b/testing/web-platform/tests/resources/test/tests/functional/promise-async.html
new file mode 100644
index 0000000000..fa82665cf0
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/promise-async.html
@@ -0,0 +1,172 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Async Tests and Promises</title>
+</head>
+<body>
+<h1>Async Tests and Promises</h1>
+<p>This test assumes ECMAScript 6 Promise support. Some failures are expected.</p>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+
+test(function() {
+ var p = new Promise(function(resolve, reject) {});
+ assert_true('then' in p);
+ assert_equals(typeof Promise.resolve, 'function');
+ assert_equals(typeof Promise.reject, 'function');
+}, "Promises are supported in your browser");
+
+(function() {
+ var t = async_test("Promise resolution");
+ t.step(function() {
+ Promise.resolve('x').then(
+ t.step_func(function(value) {
+ assert_equals(value, 'x');
+ t.done();
+ }),
+ t.unreached_func('Promise should not reject')
+ );
+ });
+}());
+
+(function() {
+ var t = async_test("Promise rejection");
+ t.step(function() {
+ Promise.reject(Error('fail')).then(
+ t.unreached_func('Promise should reject'),
+ t.step_func(function(reason) {
+ assert_true(reason instanceof Error);
+ assert_equals(reason.message, 'fail');
+ t.done();
+ })
+ );
+ });
+}());
+
+(function() {
+ var t = async_test("Promises resolution chaining");
+ t.step(function() {
+ var resolutions = [];
+ Promise.resolve('a').then(
+ t.step_func(function(value) {
+ resolutions.push(value);
+ return 'b';
+ })
+ ).then(
+ t.step_func(function(value) {
+ resolutions.push(value);
+ return 'c';
+ })
+ ).then(
+ t.step_func(function(value) {
+ resolutions.push(value);
+
+ assert_array_equals(resolutions, ['a', 'b', 'c']);
+ t.done();
+ })
+ ).catch(
+ t.unreached_func('promise should not have rejected')
+ );
+ });
+}());
+
+(function() {
+ var t = async_test("Use of step_func with Promises");
+ t.step(function() {
+ var resolutions = [];
+ Promise.resolve('x').then(
+ t.step_func_done(),
+ t.unreached_func('Promise should not have rejected')
+ );
+ });
+}());
+
+(function() {
+ var t = async_test("Promises and test assertion failures (should fail)");
+ t.step(function() {
+ var resolutions = [];
+ Promise.resolve('x').then(
+ t.step_func(function(value) {
+ assert_true(false, 'This failure is expected');
+ })
+ ).then(
+ t.unreached_func('Promise should not have resolved')
+ ).catch(
+ t.unreached_func('Promise should not have rejected')
+ );
+ });
+}());
+
+(function() {
+ var t = async_test("Use of unreached_func with Promises (should fail)");
+ t.step(function() {
+ var resolutions = [];
+ var r;
+ var p = new Promise(function(resolve, reject) {
+ // Reject instead of resolve, to demonstrate failure.
+ reject(123);
+ });
+ p.then(
+ function(value) {
+ assert_equals(value, 123, 'This should not actually happen');
+ },
+ t.unreached_func('This failure is expected')
+ );
+ });
+}());
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Promise rejection",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Promise resolution",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Promises and test assertion failures (should fail)",
+ "properties": {},
+ "message": "assert_true: This failure is expected expected true got false"
+ },
+ {
+ "status_string": "PASS",
+ "name": "Promises are supported in your browser",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Promises resolution chaining",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Use of step_func with Promises",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Use of unreached_func with Promises (should fail)",
+ "properties": {},
+ "message": "assert_unreached: This failure is expected Reached unreachable code"
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/promise-with-sync.html b/testing/web-platform/tests/resources/test/tests/functional/promise-with-sync.html
new file mode 100644
index 0000000000..e8e680a9c7
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/promise-with-sync.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Promise Tests and Synchronous Tests</title>
+</head>
+<body>
+<h1>Promise Tests</h1>
+<p>This test demonstrates the use of <tt>promise_test</tt> alongside synchronous tests.</p>
+<div id="log"></div>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+<script>
+"use strict";
+var sequence = [];
+
+test(function(t) {
+ assert_array_equals(sequence, []);
+ sequence.push(1);
+}, "first synchronous test");
+
+promise_test(function() {
+ assert_array_equals(sequence, [1, 2]);
+
+ return Promise.resolve()
+ .then(function() {
+ assert_array_equals(sequence, [1, 2]);
+ sequence.push(3);
+ });
+}, "first promise_test");;
+
+test(function(t) {
+ assert_array_equals(sequence, [1]);
+ sequence.push(2);
+}, "second synchronous test");
+
+promise_test(function() {
+ assert_array_equals(sequence, [1, 2, 3]);
+
+ return Promise.resolve()
+ .then(function() {
+ assert_array_equals(sequence, [1, 2, 3]);
+ });
+}, "second promise_test");;
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "message": null,
+ "properties": {},
+ "name": "first promise_test",
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "properties": {},
+ "name": "first synchronous test",
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "properties": {},
+ "name": "second promise_test",
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "properties": {},
+ "name": "second synchronous test",
+ "status_string": "PASS"
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/promise.html b/testing/web-platform/tests/resources/test/tests/functional/promise.html
new file mode 100644
index 0000000000..f35feb0e21
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/promise.html
@@ -0,0 +1,219 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Promise Tests</title>
+</head>
+<body>
+<h1>Promise Tests</h1>
+<p>This test demonstrates the use of <tt>promise_test</tt>. Assumes ECMAScript 6
+Promise support. Some failures are expected.</p>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(
+ function() {
+ var p = new Promise(function(resolve, reject){});
+ assert_true("then" in p);
+ assert_equals(typeof Promise.resolve, "function");
+ assert_equals(typeof Promise.reject, "function");
+ },
+ "Promises are supported in your browser");
+
+promise_test(
+ function() {
+ return Promise.resolve("x")
+ .then(
+ function(value) {
+ assert_equals(value,
+ "x",
+ "Fulfilled promise should pass result to " +
+ "fulfill reaction.");
+ });
+ },
+ "Promise fulfillment with result");
+
+promise_test(
+ function(t) {
+ return Promise.reject(new Error("fail"))
+ .then(t.unreached_func("Promise should reject"),
+ function(reason) {
+ assert_true(
+ reason instanceof Error,
+ "Rejected promise should pass reason to fulfill reaction.");
+ assert_equals(
+ reason.message,
+ "fail",
+ "Rejected promise should pass reason to reject reaction.");
+ });
+ },
+ "Promise rejection with result");
+
+promise_test(
+ function() {
+ var resolutions = [];
+ return Promise.resolve("a")
+ .then(
+ function(value) {
+ resolutions.push(value);
+ return "b";
+ })
+ .then(
+ function(value) {
+ resolutions.push(value);
+ return "c";
+ })
+ .then(
+ function(value) {
+ resolutions.push(value);
+ assert_array_equals(resolutions, ["a", "b", "c"]);
+ });
+ },
+ "Chain of promise resolutions");
+
+promise_test(
+ function(t) {
+ var resolutions = [];
+ return Promise.resolve("x")
+ .then(
+ function(value) {
+ assert_true(false, "Expected failure.");
+ })
+ .then(t.unreached_func("UNEXPECTED FAILURE: Promise should not have resolved."));
+ },
+ "Assertion failure in a fulfill reaction (should FAIL with an expected failure)");
+
+promise_test(
+ function(t) {
+ return new Promise(
+ function(resolve, reject) {
+ reject(123);
+ })
+ .then(t.unreached_func("UNEXPECTED FAILURE: Fulfill reaction reached after rejection."),
+ t.unreached_func("Expected failure."));
+ },
+ "unreached_func as reactor (should FAIL with an expected failure)");
+
+promise_test(
+ function() {
+ return true;
+ },
+ "promise_test with function that doesn't return a Promise (should FAIL)");
+
+promise_test(function(){},
+ "promise_test with function that doesn't return anything");
+
+promise_test(
+ function() { return { then: 23 }; },
+ "promise_test that returns a non-thenable (should FAIL)");
+
+promise_test(
+ function() {
+ return Promise.reject("Expected rejection");
+ },
+ "promise_test with unhandled rejection (should FAIL)");
+
+promise_test(
+ function() {
+ return Promise.resolve(10)
+ .then(
+ function(value) {
+ throw Error("Expected exception.");
+ });
+ },
+ "promise_test with unhandled exception in fulfill reaction (should FAIL)");
+
+promise_test(
+ function(t) {
+ return Promise.reject(10)
+ .then(
+ t.unreached_func("UNEXPECTED FAILURE: Fulfill reaction reached after rejection."),
+ function(value) {
+ throw Error("Expected exception.");
+ });
+ },
+ "promise_test with unhandled exception in reject reaction (should FAIL)");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "FAIL",
+ "name": "Assertion failure in a fulfill reaction (should FAIL with an expected failure)",
+ "message": "assert_true: Expected failure. expected true got false",
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Chain of promise resolutions",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Promise fulfillment with result",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Promise rejection with result",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "PASS",
+ "name": "Promises are supported in your browser",
+ "message": null,
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "promise_test with function that doesn't return a Promise (should FAIL)",
+ "message": "promise_test: test body must return a 'thenable' object (received an object with no `then` method)",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "promise_test with function that doesn't return anything",
+ "message": "promise_test: test body must return a 'thenable' object (received undefined)",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "promise_test that returns a non-thenable (should FAIL)",
+ "message": "promise_test: test body must return a 'thenable' object (received an object with no `then` method)",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "promise_test with unhandled exception in fulfill reaction (should FAIL)",
+ "message": "promise_test: Unhandled rejection with value: object \"Error: Expected exception.\"",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "promise_test with unhandled exception in reject reaction (should FAIL)",
+ "message": "promise_test: Unhandled rejection with value: object \"Error: Expected exception.\"",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "promise_test with unhandled rejection (should FAIL)",
+ "message": "promise_test: Unhandled rejection with value: \"Expected rejection\"",
+ "properties": {}
+ },
+ {
+ "status_string": "FAIL",
+ "name": "unreached_func as reactor (should FAIL with an expected failure)",
+ "message": "assert_unreached: Expected failure. Reached unreachable code",
+ "properties": {}
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/queue.html b/testing/web-platform/tests/resources/test/tests/functional/queue.html
new file mode 100644
index 0000000000..0c721286ec
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/queue.html
@@ -0,0 +1,130 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test queuing synchronous tests</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+
+<script>
+"use strict";
+var inInitialTurn = true;
+
+test(function(t) {
+ assert_true(
+ inInitialTurn, "should execute in the initial turn of the event loop"
+ );
+}, "First synchronous test");
+
+test(function(t) {
+ assert_true(
+ inInitialTurn, "should execute in the initial turn of the event loop"
+ );
+}, "Second synchronous test");
+
+async_test(function(t) {
+ assert_true(
+ inInitialTurn, "should execute in the initial turn of the event loop"
+ );
+ t.done();
+}, "First async_test (run in parallel)");
+
+async_test(function(t) {
+ assert_true(
+ inInitialTurn, "should execute in the initial turn of the event loop"
+ );
+ t.done();
+}, "Second async_test (run in parallel)");
+
+test(function(t) {
+ assert_true(
+ inInitialTurn, "should execute in the initial turn of the event loop"
+ );
+}, "Third synchronous test");
+
+promise_test(function(t) {
+ assert_false(
+ inInitialTurn, "should not execute in the initial turn of the event loop"
+ );
+
+ return Promise.resolve();
+}, "promise_test");
+
+async_test(function(t) {
+ assert_true(
+ inInitialTurn, "should execute in the initial turn of the event loop"
+ );
+ t.done();
+}, "Third async_test (run in parallel)");
+
+test(function(t) {
+ assert_true(
+ inInitialTurn, "should execute in the initial turn of the event loop"
+ );
+}, "Fourth synchronous test");
+
+inInitialTurn = false;
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "properties": {},
+ "name": "First async_test (run in parallel)",
+ "status_string": "PASS",
+ "message": null
+ },
+ {
+ "properties": {},
+ "name": "First synchronous test",
+ "status_string": "PASS",
+ "message": null
+ },
+ {
+ "properties": {},
+ "name": "Fourth synchronous test",
+ "status_string": "PASS",
+ "message": null
+ },
+ {
+ "properties": {},
+ "name": "Second async_test (run in parallel)",
+ "status_string": "PASS",
+ "message": null
+ },
+ {
+ "properties": {},
+ "name": "Second synchronous test",
+ "status_string": "PASS",
+ "message": null
+ },
+ {
+ "properties": {},
+ "name": "Third async_test (run in parallel)",
+ "status_string": "PASS",
+ "message": null
+ },
+ {
+ "properties": {},
+ "name": "Third synchronous test",
+ "status_string": "PASS",
+ "message": null
+ },
+ {
+ "properties": {},
+ "name": "promise_test",
+ "status_string": "PASS",
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/setup-function-worker.js b/testing/web-platform/tests/resources/test/tests/functional/setup-function-worker.js
new file mode 100644
index 0000000000..82c1456aa6
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/setup-function-worker.js
@@ -0,0 +1,14 @@
+importScripts("/resources/testharness.js");
+
+// Regression test for https://github.com/web-platform-tests/wpt/issues/27299,
+// where we broke the ability for a setup function in a worker to contain an
+// assertion (even a passing one).
+setup(function() {
+ assert_true(true, "True is true");
+});
+
+// We must define at least one test for the harness, though it is not what we
+// are testing here.
+test(function() {
+ assert_false(false, "False is false");
+}, 'Worker test');
diff --git a/testing/web-platform/tests/resources/test/tests/functional/setup-worker-service.html b/testing/web-platform/tests/resources/test/tests/functional/setup-worker-service.html
new file mode 100644
index 0000000000..9f24adac2d
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/setup-worker-service.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Setup function in a service worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<h1>Setup function in a service worker</h1>
+<p>This test assumes that the browser supports <a href="http://www.w3.org/TR/service-workers/">ServiceWorkers</a>.
+<div id="log"></div>
+
+<script>
+test(function(t) {
+ assert_true("serviceWorker" in navigator,
+ "navigator.serviceWorker exists");
+}, "Browser supports ServiceWorker");
+
+promise_test(function() {
+ // Since the service worker registration could be in an indeterminate
+ // state (due to, for example, a previous test run failing), we start by
+ // unregstering our service worker and then registering it again.
+ var scope = "service-worker-scope";
+ var worker_url = "setup-function-worker.js";
+
+ return navigator.serviceWorker.register(worker_url, {scope: scope})
+ .then(function(registration) {
+ return registration.unregister();
+ }).then(function() {
+ return navigator.serviceWorker.register(worker_url, {scope: scope});
+ }).then(function(registration) {
+ add_completion_callback(function() {
+ registration.unregister();
+ });
+
+ return new Promise(function(resolve) {
+ registration.addEventListener("updatefound", function() {
+ resolve(registration.installing);
+ });
+ });
+ }).then(function(worker) {
+ fetch_tests_from_worker(worker);
+ });
+}, "Register ServiceWorker");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Browser supports ServiceWorker",
+ "properties": {},
+ "message": null
+ },
+ {
+ "message": null,
+ "name": "Register ServiceWorker",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "Worker test",
+ "properties": {},
+ "status_string": "PASS"
+ }
+ ],
+ "summarized_asserts": [
+ {
+ "assert_name": "assert_true",
+ "test": "Browser supports ServiceWorker",
+ "args": [
+ "true",
+ "\"navigator.serviceWorker exists\""
+ ],
+ "status": 0
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/single-page-test-fail.html b/testing/web-platform/tests/resources/test/tests/functional/single-page-test-fail.html
new file mode 100644
index 0000000000..8bbd530c48
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/single-page-test-fail.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<title>Example with file_is_test (should fail)</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+setup({ single_test: true });
+onload = function() {
+ assert_true(false);
+ done();
+}
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "FAIL",
+ "name": "Example with file_is_test (should fail)",
+ "properties": {},
+ "message": "uncaught exception: Error: assert_true: expected true got false"
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/single-page-test-no-assertions.html b/testing/web-platform/tests/resources/test/tests/functional/single-page-test-no-assertions.html
new file mode 100644
index 0000000000..9b39d2a02c
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/single-page-test-no-assertions.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<title>Example single page test with no asserts</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+setup({ single_test: true });
+done();
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Example single page test with no asserts",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/single-page-test-no-body.html b/testing/web-platform/tests/resources/test/tests/functional/single-page-test-no-body.html
new file mode 100644
index 0000000000..cb018f4dae
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/single-page-test-no-body.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<title>Example single page test with no body</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+setup({ single_test: true });
+assert_true(true);
+done();
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Example single page test with no body",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/single-page-test-pass.html b/testing/web-platform/tests/resources/test/tests/functional/single-page-test-pass.html
new file mode 100644
index 0000000000..e143e22f3c
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/single-page-test-pass.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<title>Example with file_is_test</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+setup({ single_test: true });
+onload = function() {
+ assert_true(true);
+ done();
+}
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Example with file_is_test",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/step_wait.html b/testing/web-platform/tests/resources/test/tests/functional/step_wait.html
new file mode 100644
index 0000000000..ae3442c759
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/step_wait.html
@@ -0,0 +1,57 @@
+<!doctype html>
+<title>Tests for step_wait</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+promise_test(async t => {
+ let x = 1;
+ Promise.resolve().then(() => ++x);
+ await t.step_wait(() => x === 1);
+ assert_equals(x, 2);
+}, "Basic step_wait() test");
+
+promise_test(async t => {
+ let cond = false;
+ let x = 0;
+ setTimeout(() => cond = true, 100);
+ await t.step_wait(() => {
+ ++x;
+ return cond;
+ });
+ assert_equals(x, 2);
+}, "step_wait() isn't invoked too often");
+
+promise_test(async t => {
+ await t.step_wait(); // Throws
+}, "step_wait() takes an argument");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_tests": [
+ {
+ "name": "Basic step_wait() test",
+ "message": null,
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "name": "step_wait() isn't invoked too often",
+ "message": null,
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "name": "step_wait() takes an argument",
+ "message": "cond is not a function",
+ "properties": {},
+ "status_string": "FAIL"
+ }
+ ],
+ "type": "complete",
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ }
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/step_wait_func.html b/testing/web-platform/tests/resources/test/tests/functional/step_wait_func.html
new file mode 100644
index 0000000000..9fed18a3e2
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/step_wait_func.html
@@ -0,0 +1,49 @@
+<!doctype html>
+<title>Tests for step_wait_func and step_wait_func_done</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<div id=log></div>
+<script>
+async_test(t => {
+ let x = 0;
+ let step_x = 0;
+ setTimeout(() => ++x, 100);
+ t.step_wait_func(() => {
+ ++step_x;
+ return x === 1;
+ }, () => {
+ assert_equals(step_x, 2);
+ t.done();
+ });
+}, "Basic step_wait_func() test");
+
+async_test(t => {
+ let x = 0;
+ setTimeout(() => ++x, 100);
+ t.step_wait_func_done(() => true, () => assert_equals(x, 0));
+}, "Basic step_wait_func_done() test");
+
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "properties": {},
+ "message": null,
+ "name": "Basic step_wait_func() test",
+ "status_string": "PASS"
+ },
+ {
+ "properties": {},
+ "message": null,
+ "name": "Basic step_wait_func_done() test",
+ "status_string": "PASS"
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/task-scheduling-promise-test.html b/testing/web-platform/tests/resources/test/tests/functional/task-scheduling-promise-test.html
new file mode 100644
index 0000000000..9d8e5c11cc
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/task-scheduling-promise-test.html
@@ -0,0 +1,241 @@
+<!doctype html>
+<title>testharness.js - task scheduling</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+<script>
+var sameTask = null;
+var sameMicrotask = null;
+var expectedError = new Error('This error is expected');
+
+promise_test(function() {
+ return Promise.resolve()
+ .then(function() {
+ sameMirotask = true;
+ Promise.resolve().then(() => sameMicrotask = false);
+ });
+}, 'promise test without cleanup #1');
+
+promise_test(function() {
+ assert_false(sameMicrotask);
+
+ return Promise.resolve();
+}, 'sub-test with 0 cleanup functions executes in distinct microtask from a passing sub-test');
+
+promise_test(function() {
+ return Promise.resolve()
+ .then(function() {
+ sameMirotask = true;
+ Promise.resolve().then(() => sameMicrotask = false);
+ throw expectedError;
+ });
+}, 'failing promise test without cleanup #1');
+
+promise_test(function() {
+ assert_false(sameMicrotask);
+
+ return Promise.resolve();
+}, 'sub-test with 0 cleanup functions executes in distinct microtask from a failing sub-test');
+
+promise_test(function(t) {
+ t.add_cleanup(function() {});
+
+ return Promise.resolve()
+ .then(function() {
+ sameMirotask = true;
+ Promise.resolve().then(() => sameMicrotask = false);
+ });
+}, 'promise test with cleanup #1');
+
+promise_test(function() {
+ assert_false(sameMicrotask);
+
+ return Promise.resolve();
+}, 'sub-test with some cleanup functions executes in distinct microtask from a passing sub-test');
+
+promise_test(function(t) {
+ t.add_cleanup(function() {});
+
+ return Promise.resolve()
+ .then(function() {
+ sameMirotask = true;
+ Promise.resolve().then(() => sameMicrotask = false);
+ throw expectedError;
+ });
+}, 'failing promise test with cleanup #1');
+
+promise_test(function() {
+ assert_false(sameMicrotask);
+
+ return Promise.resolve();
+}, 'sub-test with some cleanup functions executes in distinct microtask from a failing sub-test');
+
+promise_test(function(t) {
+ return Promise.resolve()
+ .then(function() {
+ sameTask = true;
+ t.step_timeout(() => sameTask = false, 0);
+ });
+}, 'promise test without cleanup #2');
+
+promise_test(function() {
+ assert_true(sameTask);
+
+ return Promise.resolve();
+}, 'sub-test with 0 cleanup functions executes in the same task as a passing sub-test');
+
+promise_test(function(t) {
+ return Promise.resolve()
+ .then(function() {
+ sameTask = true;
+ t.step_timeout(() => sameTask = false, 0);
+ throw expectedError;
+ });
+}, 'failing promise test without cleanup #2');
+
+promise_test(function() {
+ assert_true(sameTask);
+
+ return Promise.resolve();
+}, 'sub-test with 0 cleanup functions executes in the same task as a failing sub-test');
+
+promise_test(function(t) {
+ t.add_cleanup(function() {});
+
+ return Promise.resolve()
+ .then(function() {
+ sameTask = true;
+ t.step_timeout(() => sameTask = false, 0);
+ });
+}, 'promise test with cleanup #2');
+
+promise_test(function() {
+ assert_true(sameTask);
+
+ return Promise.resolve();
+}, 'sub-test with some cleanup functions executes in the same task as a passing sub-test');
+
+promise_test(function(t) {
+ t.add_cleanup(function() {});
+
+ return Promise.resolve()
+ .then(function() {
+ sameTask = true;
+ t.step_timeout(() => sameTask = false, 0);
+ throw expectedError;
+ });
+}, 'failing promise test with cleanup #2');
+
+promise_test(function() {
+ assert_true(sameTask);
+
+ return Promise.resolve();
+}, 'sub-test with some cleanup functions executes in the same task as a failing sub-test');
+</script>
+
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "message": "promise_test: Unhandled rejection with value: object \"Error: This error is expected\"",
+ "name": "failing promise test with cleanup #1",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": "promise_test: Unhandled rejection with value: object \"Error: This error is expected\"",
+ "name": "failing promise test with cleanup #2",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": "promise_test: Unhandled rejection with value: object \"Error: This error is expected\"",
+ "name": "failing promise test without cleanup #1",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": "promise_test: Unhandled rejection with value: object \"Error: This error is expected\"",
+ "name": "failing promise test without cleanup #2",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": null,
+ "name": "promise test with cleanup #1",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "promise test with cleanup #2",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "promise test without cleanup #1",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "promise test without cleanup #2",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with 0 cleanup functions executes in distinct microtask from a failing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with 0 cleanup functions executes in distinct microtask from a passing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with 0 cleanup functions executes in the same task as a failing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with 0 cleanup functions executes in the same task as a passing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with some cleanup functions executes in distinct microtask from a failing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with some cleanup functions executes in distinct microtask from a passing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with some cleanup functions executes in the same task as a failing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with some cleanup functions executes in the same task as a passing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/task-scheduling-test.html b/testing/web-platform/tests/resources/test/tests/functional/task-scheduling-test.html
new file mode 100644
index 0000000000..035844448d
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/task-scheduling-test.html
@@ -0,0 +1,141 @@
+<!doctype html>
+<title>testharness.js - task scheduling</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+<script>
+var sameMicrotask = null;
+var expectedError = new Error('This error is expected');
+
+// Derived from `immediate`
+// https://github.com/calvinmetcalf/immediate/blob/c353bd2106648cee1d525bfda22cfc4456e69c0e/lib/mutation.js
+function microTask(callback) {
+ var observer = new MutationObserver(callback);
+ var element = document.createTextNode('');
+ observer.observe(element, {
+ characterData: true
+ });
+
+ element.data = true;
+};
+
+async_test(function(t) {
+ var microtask_ran = false;
+
+ t.step_timeout(t.step_func(function() {
+ assert_true(microtask_ran, 'function registered as a microtask was executed before task');
+ t.done();
+ }), 0);
+
+ microTask(function() {
+ microtask_ran = true;
+ });
+}, 'precondition: microtask creation logic functions as expected');
+
+test(function() {
+ sameMicrotask = true;
+ microTask(function() { sameMicrotask = false; });
+}, 'synchronous test without cleanup');
+
+test(function() {
+ assert_true(sameMicrotask);
+}, 'sub-test with 0 cleanup functions executes in the same microtask as a passing sub-test');
+
+test(function() {
+ sameMicrotask = true;
+ microTask(function() { sameMicrotask = false; });
+ throw expectedError;
+}, 'failing synchronous test without cleanup');
+
+test(function() {
+ assert_true(sameMicrotask);
+}, 'sub-test with 0 cleanup functions executes in the same microtask as a failing sub-test');
+
+test(function(t) {
+ t.add_cleanup(function() {});
+
+ sameMicrotask = true;
+ microTask(function() { sameMicrotask = false; });
+}, 'synchronous test with cleanup');
+
+test(function() {
+ assert_true(sameMicrotask);
+}, 'sub-test with some cleanup functions executes in the same microtask as a passing sub-test');
+
+test(function(t) {
+ t.add_cleanup(function() {});
+
+ sameMicrotask = true;
+ microTask(function() { sameMicrotask = false; });
+ throw expectedError;
+}, 'failing synchronous test with cleanup');
+
+test(function() {
+ assert_true(sameMicrotask);
+}, 'sub-test with some cleanup functions executes in the same microtask as a failing sub-test');
+</script>
+
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "message": null,
+ "status_string": "OK"
+ },
+ "summarized_tests": [
+ {
+ "message": "This error is expected",
+ "name": "failing synchronous test with cleanup",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": "This error is expected",
+ "name": "failing synchronous test without cleanup",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": null,
+ "name": "precondition: microtask creation logic functions as expected",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with 0 cleanup functions executes in the same microtask as a failing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with 0 cleanup functions executes in the same microtask as a passing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with some cleanup functions executes in the same microtask as a failing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "sub-test with some cleanup functions executes in the same microtask as a passing sub-test",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "synchronous test with cleanup",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "synchronous test without cleanup",
+ "properties": {},
+ "status_string": "PASS"
+ }
+ ],
+ "type": "complete"
+}
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/uncaught-exception-handle.html b/testing/web-platform/tests/resources/test/tests/functional/uncaught-exception-handle.html
new file mode 100644
index 0000000000..764b0c4055
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/uncaught-exception-handle.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Harness Handling Uncaught Exception</title>
+</head>
+<script src="/resources/testharness.js"></script>
+
+<body>
+<h1>Harness Handling Uncaught Exception</h1>
+<div id="log"></div>
+<script>
+var t = async_test("This should show a harness status of 'Error' and a test status of 'Not Run'");
+throw new Error("Example Error");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Error: Example Error"
+ },
+ "summarized_tests": [
+ {
+ "status_string": "NOTRUN",
+ "name": "This should show a harness status of 'Error' and a test status of 'Not Run'",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/uncaught-exception-ignore.html b/testing/web-platform/tests/resources/test/tests/functional/uncaught-exception-ignore.html
new file mode 100644
index 0000000000..6bd0ddbb0d
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/uncaught-exception-ignore.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Harness Ignoring Uncaught Exception</title>
+</head>
+<script src="/resources/testharness.js"></script>
+
+<body>
+<h1>Harness Ignoring Uncaught Exception</h1>
+<div id="log"></div>
+<script>
+setup({allow_uncaught_exception:true});
+var t = async_test("setup({allow_uncaught_exception:true}) should allow tests to pass even if there is an exception");
+onerror = t.step_func(function() {t.done()});
+throw new Error("Example Error");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "setup({allow_uncaught_exception:true}) should allow tests to pass even if there is an exception",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/worker-dedicated-uncaught-allow.html b/testing/web-platform/tests/resources/test/tests/functional/worker-dedicated-uncaught-allow.html
new file mode 100644
index 0000000000..ba28d4914f
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/worker-dedicated-uncaught-allow.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Dedicated Worker Tests - Allowed Uncaught Exception</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<h1>Dedicated Web Worker Tests - Allowed Uncaught Exception</h1>
+<p>Demonstrates running <tt>testharness</tt> based tests inside a dedicated web worker.
+<p>The test harness is expected to pass despite an uncaught exception in a worker because that worker is configured to allow uncaught exceptions.</p>
+<div id="log"></div>
+
+<script>
+test(function(t) {
+ assert_true("Worker" in self, "Browser should support Workers");
+ },
+ "Browser supports Workers");
+
+fetch_tests_from_worker(new Worker("worker-uncaught-allow.js"));
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Browser supports Workers",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "onerror event is triggered",
+ "properties": {},
+ "message": null
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/worker-dedicated-uncaught-single.html b/testing/web-platform/tests/resources/test/tests/functional/worker-dedicated-uncaught-single.html
new file mode 100644
index 0000000000..486e067114
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/worker-dedicated-uncaught-single.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Dedicated Worker Tests - Uncaught Exception in Single-Page Test</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<h1>Dedicated Web Worker Tests - Uncaught Exception in Single-Page Test</h1>
+<p>Demonstrates running <tt>testharness</tt> based tests inside a dedicated web worker.
+<p>The test harness is expected to pass despite an uncaught exception in a worker because that worker is a single-page test.</p>
+<div id="log"></div>
+
+<script>
+test(function(t) {
+ assert_true("Worker" in self, "Browser should support Workers");
+ },
+ "Browser supports Workers");
+
+fetch_tests_from_worker(new Worker("worker-uncaught-single.js"));
+
+test(function(t) {
+ assert_false(false, "False is false");
+ },
+ "Test running on main document.");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "OK",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Browser supports Workers",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test running on main document.",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "FAIL",
+ "name": "worker-uncaught-single",
+ "properties": {},
+ "message": "Error: This failure is expected."
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/worker-dedicated.sub.html b/testing/web-platform/tests/resources/test/tests/functional/worker-dedicated.sub.html
new file mode 100644
index 0000000000..efd703c760
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/worker-dedicated.sub.html
@@ -0,0 +1,88 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Dedicated Worker Tests</title>
+<script src="../../../testharness.js"></script>
+<script src="../../../testharnessreport.js"></script>
+</head>
+<body>
+<h1>Dedicated Web Worker Tests</h1>
+<p>Demonstrates running <tt>testharness</tt> based tests inside a dedicated web worker.
+<p>The test harness is expected to fail due to an uncaught exception in one worker.</p>
+<div id="log"></div>
+
+<script>
+test(function(t) {
+ assert_true("Worker" in self, "Browser should support Workers");
+ },
+ "Browser supports Workers");
+
+fetch_tests_from_worker(new Worker("worker.js"));
+
+fetch_tests_from_worker(new Worker("worker-error.js"));
+
+test(function(t) {
+ assert_false(false, "False is false");
+ },
+ "Test running on main document.");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "ERROR",
+ "message": "Error: This failure is expected."
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Browser supports Workers",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Test running on main document.",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Worker async_test that completes successfully",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Worker test that completes successfully",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "worker test that completes successfully before exception",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "Worker test that doesn't run ('NOT RUN')",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Worker test that fails ('FAIL')",
+ "properties": {},
+ "message": "assert_true: Failing test expected true got false"
+ },
+ {
+ "status_string": "TIMEOUT",
+ "name": "Worker test that times out ('TIMEOUT')",
+ "properties": {},
+ "message": "Test timed out"
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/worker-error.js b/testing/web-platform/tests/resources/test/tests/functional/worker-error.js
new file mode 100644
index 0000000000..7b89602f04
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/worker-error.js
@@ -0,0 +1,8 @@
+importScripts("/resources/testharness.js");
+
+// The following sub-test ensures that the worker is not interpreted as a
+// single-page test. The subsequent uncaught exception should therefore be
+// interpreted as a harness error rather than a single-page test failure.
+test(function() {}, "worker test that completes successfully before exception");
+
+throw new Error("This failure is expected.");
diff --git a/testing/web-platform/tests/resources/test/tests/functional/worker-service.html b/testing/web-platform/tests/resources/test/tests/functional/worker-service.html
new file mode 100644
index 0000000000..2e07746e62
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/worker-service.html
@@ -0,0 +1,115 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Example with a service worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<h1>Service Worker Tests</h1>
+<p>Demonstrates running <tt>testharness</tt> based tests inside a service worker.
+<p>The test harness should time out due to one of the tests inside the worker timing out.
+<p>This test assumes that the browser supports <a href="http://www.w3.org/TR/service-workers/">ServiceWorkers</a>.
+<div id="log"></div>
+
+<script>
+test(
+ function(t) {
+ assert_true("serviceWorker" in navigator,
+ "navigator.serviceWorker exists");
+ },
+ "Browser supports ServiceWorker");
+
+promise_test(
+ function() {
+ // Since the service worker registration could be in an indeterminate
+ // state (due to, for example, a previous test run failing), we start by
+ // unregstering our service worker and then registering it again.
+ var scope = "service-worker-scope";
+ var worker_url = "worker.js";
+
+ return navigator.serviceWorker.register(worker_url, {scope: scope})
+ .then(
+ function(registration) {
+ return registration.unregister();
+ })
+ .then(
+ function() {
+ return navigator.serviceWorker.register(worker_url, {scope: scope});
+ })
+ .then(
+ function(registration) {
+ add_completion_callback(
+ function() {
+ registration.unregister();
+ });
+
+ return new Promise(
+ function(resolve) {
+ registration.addEventListener("updatefound",
+ function() {
+ resolve(registration.installing);
+ });
+ });
+ })
+ .then(
+ function(worker) {
+ fetch_tests_from_worker(worker);
+ });
+ },
+ "Register ServiceWorker");
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "TIMEOUT",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Browser supports ServiceWorker",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Register ServiceWorker",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Worker async_test that completes successfully",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "PASS",
+ "name": "Worker test that completes successfully",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "NOTRUN",
+ "name": "Worker test that doesn't run ('NOT RUN')",
+ "properties": {},
+ "message": null
+ },
+ {
+ "status_string": "FAIL",
+ "name": "Worker test that fails ('FAIL')",
+ "properties": {},
+ "message": "assert_true: Failing test expected true got false"
+ },
+ {
+ "status_string": "TIMEOUT",
+ "name": "Worker test that times out ('TIMEOUT')",
+ "properties": {},
+ "message": "Test timed out"
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/worker-shared.html b/testing/web-platform/tests/resources/test/tests/functional/worker-shared.html
new file mode 100644
index 0000000000..e26f17dec2
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/worker-shared.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Example with a shared worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<h1>Shared Web Worker Tests</h1>
+<p>Demonstrates running <tt>testharness</tt> based tests inside a shared worker.
+<p>The test harness should time out due to one of the tests in the worker timing out.
+<p>This test assumes that the browser supports <a href="http://www.w3.org/TR/workers/#shared-workers-and-the-sharedworker-interface">shared web workers</a>.
+<div id="log"></div>
+
+<script>
+test(
+ function(t) {
+ assert_true("SharedWorker" in self,
+ "Browser should support SharedWorkers");
+ },
+ "Browser supports SharedWorkers");
+
+fetch_tests_from_worker(new SharedWorker("worker.js",
+ "My shared worker"));
+</script>
+<script type="text/json" id="expected">
+{
+ "summarized_status": {
+ "status_string": "TIMEOUT",
+ "message": null
+ },
+ "summarized_tests": [
+ {
+ "status_string": "PASS",
+ "name": "Browser supports SharedWorkers",
+ "properties": {},
+ "message": null
+ },
+ {
+ "message": null,
+ "name": "Worker async_test that completes successfully",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "Worker test that completes successfully",
+ "properties": {},
+ "status_string": "PASS"
+ },
+ {
+ "message": null,
+ "name": "Worker test that doesn't run ('NOT RUN')",
+ "properties": {},
+ "status_string": "NOTRUN"
+ },
+ {
+ "message": "assert_true: Failing test expected true got false",
+ "name": "Worker test that fails ('FAIL')",
+ "properties": {},
+ "status_string": "FAIL"
+ },
+ {
+ "message": "Test timed out",
+ "name": "Worker test that times out ('TIMEOUT')",
+ "properties": {},
+ "status_string": "TIMEOUT"
+ }
+ ],
+ "type": "complete"
+}
+</script>
+</body>
diff --git a/testing/web-platform/tests/resources/test/tests/functional/worker-uncaught-allow.js b/testing/web-platform/tests/resources/test/tests/functional/worker-uncaught-allow.js
new file mode 100644
index 0000000000..6925d59349
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/worker-uncaught-allow.js
@@ -0,0 +1,19 @@
+importScripts("/resources/testharness.js");
+
+setup({allow_uncaught_exception:true});
+
+async_test(function(t) {
+ onerror = function() {
+ // Further delay the test's completion to ensure that the worker's
+ // `onerror` handler does not influence results in the parent context.
+ setTimeout(function() {
+ t.done();
+ }, 0);
+ };
+
+ setTimeout(function() {
+ throw new Error("This error is expected.");
+ }, 0);
+}, 'onerror event is triggered');
+
+done();
diff --git a/testing/web-platform/tests/resources/test/tests/functional/worker-uncaught-single.js b/testing/web-platform/tests/resources/test/tests/functional/worker-uncaught-single.js
new file mode 100644
index 0000000000..c04542b2f5
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/worker-uncaught-single.js
@@ -0,0 +1,8 @@
+importScripts("/resources/testharness.js");
+
+setup({ single_test: true });
+
+// Because this script enables the `single_test` configuration option, it
+// should be interpreted as a single-page test, and the uncaught exception
+// should be reported as a test failure (harness status: OK).
+throw new Error("This failure is expected.");
diff --git a/testing/web-platform/tests/resources/test/tests/functional/worker.js b/testing/web-platform/tests/resources/test/tests/functional/worker.js
new file mode 100644
index 0000000000..a923bc2d89
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/functional/worker.js
@@ -0,0 +1,34 @@
+importScripts("/resources/testharness.js");
+
+test(
+ function(test) {
+ assert_true(true, "True is true");
+ },
+ "Worker test that completes successfully");
+
+test(
+ function(test) {
+ assert_true(false, "Failing test");
+ },
+ "Worker test that fails ('FAIL')");
+
+async_test(
+ function(test) {
+ assert_true(true, "True is true");
+ },
+ "Worker test that times out ('TIMEOUT')");
+
+async_test("Worker test that doesn't run ('NOT RUN')");
+
+async_test(
+ function(test) {
+ self.setTimeout(
+ function() {
+ test.done();
+ },
+ 0);
+ },
+ "Worker async_test that completes successfully");
+
+// An explicit done() is required for dedicated and shared web workers.
+done();
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlArray/is_json_type.html b/testing/web-platform/tests/resources/test/tests/unit/IdlArray/is_json_type.html
new file mode 100644
index 0000000000..18e83a8e89
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlArray/is_json_type.html
@@ -0,0 +1,192 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>IdlArray.prototype.is_json_type()</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+ "use strict";
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_true(idl.is_json_type(typeFrom("DOMString")));
+ assert_true(idl.is_json_type(typeFrom("ByteString")));
+ assert_true(idl.is_json_type(typeFrom("USVString")));
+ idl.add_untested_idls('enum BarEnum { "a", "b", "c" };');
+ assert_true(idl.is_json_type(typeFrom("BarEnum")));
+ }, 'should return true for all string types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_false(idl.is_json_type(typeFrom("Error")));
+ assert_false(idl.is_json_type(typeFrom("DOMException")));
+ }, 'should return false for all exception types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_false(idl.is_json_type(typeFrom("Int8Array")));
+ assert_false(idl.is_json_type(typeFrom("Int16Array")));
+ assert_false(idl.is_json_type(typeFrom("Int32Array")));
+ assert_false(idl.is_json_type(typeFrom("Uint8Array")));
+ assert_false(idl.is_json_type(typeFrom("Uint16Array")));
+ assert_false(idl.is_json_type(typeFrom("Uint32Array")));
+ assert_false(idl.is_json_type(typeFrom("Uint8ClampedArray")));
+ assert_false(idl.is_json_type(typeFrom("BigInt64Array")));
+ assert_false(idl.is_json_type(typeFrom("BigUint64Array")));
+ assert_false(idl.is_json_type(typeFrom("Float32Array")));
+ assert_false(idl.is_json_type(typeFrom("Float64Array")));
+ assert_false(idl.is_json_type(typeFrom("ArrayBuffer")));
+ assert_false(idl.is_json_type(typeFrom("DataView")));
+ }, 'should return false for all buffer source types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_true(idl.is_json_type(typeFrom("boolean")));
+ }, 'should return true for boolean');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_true(idl.is_json_type(typeFrom("byte")));
+ assert_true(idl.is_json_type(typeFrom("octet")));
+ assert_true(idl.is_json_type(typeFrom("short")));
+ assert_true(idl.is_json_type(typeFrom("unsigned short")));
+ assert_true(idl.is_json_type(typeFrom("long")));
+ assert_true(idl.is_json_type(typeFrom("unsigned long")));
+ assert_true(idl.is_json_type(typeFrom("long long")));
+ assert_true(idl.is_json_type(typeFrom("unsigned long long")));
+ assert_true(idl.is_json_type(typeFrom("float")));
+ assert_true(idl.is_json_type(typeFrom("unrestricted float")));
+ assert_true(idl.is_json_type(typeFrom("double")));
+ assert_true(idl.is_json_type(typeFrom("unrestricted double")));
+ }, 'should return true for all numeric types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_false(idl.is_json_type(typeFrom("Promise<DOMString>")));
+ }, 'should return false for promises');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_false(idl.is_json_type(typeFrom("sequence<DOMException>")));
+ assert_true(idl.is_json_type(typeFrom("sequence<DOMString>")));
+ }, 'should handle sequences according to their inner types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_false(idl.is_json_type(typeFrom("FrozenArray<DOMException>")));
+ assert_true(idl.is_json_type(typeFrom("FrozenArray<DOMString>")));
+ }, 'should handle frozen arrays according to their inner types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_true(idl.is_json_type(typeFrom("record<DOMString, DOMString>")));
+ assert_false(idl.is_json_type(typeFrom("record<DOMString, Error>")));
+ }, 'should handle records according to their inner types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_true(idl.is_json_type(typeFrom("object")));
+ }, 'should return true for object type');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_false(idl.is_json_type(typeFrom("any")));
+ }, 'should return false for any type');
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_untested_idls('dictionary Foo { DOMString foo; }; dictionary Bar : Foo { DOMString bar; };');
+ assert_true(idl.is_json_type(typeFrom("Foo")));
+ assert_true(idl.is_json_type(typeFrom("Bar")));
+ }, 'should return true for dictionaries whose members are all JSON types');
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_untested_idls('dictionary Foo { };');
+ assert_true(idl.is_json_type(typeFrom("Foo")));
+ }, 'should return true for dictionaries which have no members');
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_untested_idls('dictionary FooBar { DOMString a; Error b; }; dictionary Baz : FooBar {};');
+ assert_false(idl.is_json_type(typeFrom("FooBar")));
+ assert_false(idl.is_json_type(typeFrom("Baz")));
+ }, 'should return false for dictionaries whose members are not all JSON types');
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_untested_idls('interface Foo { DOMString toJSON(); };');
+ assert_true(idl.is_json_type(typeFrom("Foo")));
+ }, 'should return true for interfaces which declare a toJSON operation');
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_untested_idls('interface Foo { DOMString toJSON(); }; interface Bar : Foo { };');
+ assert_true(idl.is_json_type(typeFrom("Bar")));
+ }, 'should return true for interfaces which inherit from an interface which declares a toJSON operation');
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_untested_idls('interface Foo { }; interface mixin Bar { DOMString toJSON(); }; Foo includes Bar;');
+ idl.merge_mixins();
+ assert_true(idl.is_json_type(typeFrom("Foo")));
+ }, 'should return true for interfaces which mixin an interface which declare a toJSON operation');
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_untested_idls('interface Foo { };');
+ assert_false(idl.is_json_type(typeFrom("Foo")));
+ }, 'should return false for interfaces which do not declare a toJSON operation');
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_untested_idls('interface Foo { object toJSON(); };');
+ assert_true(idl.is_json_type(typeFrom("(Foo or DOMString)")));
+ }, 'should return true for union types whose member types are JSON types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_false(idl.is_json_type(typeFrom("(DataView or DOMString)")));
+ }, 'should return false for union types whose member types are not all JSON types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_true(idl.is_json_type(typeFrom("DOMString?")));
+ assert_false(idl.is_json_type(typeFrom("DataView?")));
+ }, 'should consider the inner types of nullable types');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_true(idl.is_json_type(typeFrom("[XAttr] long")));
+ assert_false(idl.is_json_type(typeFrom("[XAttr] DataView")));
+ }, 'should consider the inner types of annotated types.');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_throws_js(Error, _ => idl.is_json_type(typeFrom("Foo")));
+ }, "should throw if it references a dictionary, enum or interface which wasn't added to the IdlArray");
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_untested_idls('interface Foo : Bar { };');
+ assert_throws_js(Error, _ => idl.is_json_type(typeFrom("Foo")));
+ }, "should throw for interfaces which inherit from another interface which wasn't added to the IdlArray");
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_true(idl.is_json_type(typedefFrom("typedef double DOMHighResTimeStamp;").idlType));
+ }, 'should return true for typedefs whose source type is a JSON type');
+
+ test(function() {
+ var idl = new IdlArray();
+ assert_false(idl.is_json_type(typedefFrom("typedef DataView DOMHighResTimeStamp;").idlType));
+ }, 'should return false for typedefs whose source type is not a JSON type');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlDictionary/get_reverse_inheritance_stack.html b/testing/web-platform/tests/resources/test/tests/unit/IdlDictionary/get_reverse_inheritance_stack.html
new file mode 100644
index 0000000000..418bcdec92
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlDictionary/get_reverse_inheritance_stack.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>IdlDictionary.prototype.get_reverse_inheritance_stack()</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+ "use strict";
+ test(function() {
+ var stack = dictionaryFrom('dictionary A { };').get_reverse_inheritance_stack();
+ assert_array_equals(stack.map(d => d.name), ["A"]);
+ }, 'should return an array that includes itself.');
+
+ test(function() {
+ var d = dictionaryFrom('dictionary A : B { };');
+ assert_throws_js(Error, _ => d.get_reverse_inheritance_stack());
+ }, "should throw for dictionaries which inherit from another dictionary which wasn't added to the IdlArray");
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_idls('dictionary A : B { };');
+ idl.add_untested_idls('dictionary B : C { }; dictionary C { };');
+ var A = idl.members["A"];
+ assert_array_equals(A.get_reverse_inheritance_stack().map(d => d.name), ["C", "B", "A"]);
+ }, 'should return an array of dictionaries in order of inheritance, starting with the base dictionary');
+
+ test(function () {
+ let i = new IdlArray();
+ i.add_untested_idls('dictionary A : B {};');
+ i.assert_throws(new IdlHarnessError('A inherits B, but B is undefined.'), i => i.test());
+ }, 'A : B with B undeclared should throw IdlHarnessError');
+
+ test(function () {
+ let i = new IdlArray();
+ i.add_untested_idls('dictionary A : B {};');
+ i.add_untested_idls('interface B {};');
+ i.assert_throws(new IdlHarnessError('A inherits B, but A is not an interface.'), i => i.test());
+ }, 'dictionary A : B with B interface should throw IdlHarnessError');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlDictionary/test_partial_dictionary.html b/testing/web-platform/tests/resources/test/tests/unit/IdlDictionary/test_partial_dictionary.html
new file mode 100644
index 0000000000..d6137f6895
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlDictionary/test_partial_dictionary.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>idlharness: partial dictionaries</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+ <script src="../../../idl-helper.js"></script>
+</head>
+
+<body>
+<pre id='idl'>
+dictionary A {};
+partial dictionary A {
+ boolean B;
+};
+partial dictionary A {
+ boolean C;
+};
+</pre>
+
+<script>
+'use strict';
+
+test(() => {
+ let idlArray = new IdlArray();
+ idlArray.add_idls(document.getElementById('idl').textContent);
+ idlArray.test();
+
+ let members = idlArray.members["A"].members.map(m => m.name);
+ assert_array_equals(members, ["B", "C"], 'A should contain B, C');
+}, 'Partial dictionaries');
+</script>
+
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/constructors.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/constructors.html
new file mode 100644
index 0000000000..e9ee3f8680
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/constructors.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<title>IdlInterface.prototype.constructors()</title>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+"use strict";
+// [Constructor] extended attribute should not be supported:
+test(function() {
+ var i = interfaceFrom('[Constructor] interface A { };');
+ assert_equals(i.constructors().length, 0);
+}, 'Interface with Constructor extended attribute.');
+
+test(function() {
+ var i = interfaceFrom('interface A { constructor(); };');
+ assert_equals(i.constructors().length, 1);
+}, 'Interface with constructor method');
+
+test(function() {
+ var i = interfaceFrom('interface A { constructor(); constructor(any value); };');
+ assert_equals(i.constructors().length, 2);
+}, 'Interface with constructor overloads');
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/default_to_json_operation.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/default_to_json_operation.html
new file mode 100644
index 0000000000..5ade7d0d28
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/default_to_json_operation.html
@@ -0,0 +1,114 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>IdlDictionary.prototype.default_to_json_operation()</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+ "use strict";
+ test(function() {
+ var map = interfaceFrom('interface A { [Default] object toJSON(); };').default_to_json_operation();
+ assert_equals(map.size, 0);
+ }, 'should return an empty map when there are no attributes');
+
+ test(function() {
+ var r = interfaceFrom('interface A { };').default_to_json_operation();
+ assert_equals(r, null);
+ }, 'should return null when there is no toJSON method');
+
+ test(function() {
+ var r = interfaceFrom('interface A { DOMString toJSON(); };').default_to_json_operation();
+ assert_equals(r, null);
+ }, 'should return null when there is a toJSON method but it does not have the [Default] extended attribute');
+
+ test(function() {
+ var context = new IdlArray();
+ context.add_idls("interface A : B { DOMString toJSON(); };");
+ context.add_idls("interface B { [Default] object toJSON(); };");
+ var r = context.members.A.default_to_json_operation();
+ assert_equals(r, null);
+ }, 'should return null when there is a toJSON method but it does not have the [Default] extended attribute even if this extended attribute exists on inherited interfaces');
+
+ test(function() {
+ var map = interfaceFrom('interface A { [Default] object toJSON(); static attribute DOMString foo; };').default_to_json_operation();
+ assert_equals(map.size, 0);
+ }, 'should not include static attributes');
+
+ test(function() {
+ var map = interfaceFrom('interface A { [Default] object toJSON(); attribute Promise<DOMString> bar; };').default_to_json_operation();
+ assert_equals(map.size, 0);
+ }, 'should not include attributes which are not JSON types');
+
+ test(function() {
+ var map = interfaceFrom('interface A { [Default] object toJSON(); DOMString bar(); };').default_to_json_operation();
+ assert_equals(map.size, 0);
+ }, 'should not include operations');
+
+ test(function() {
+ var map = interfaceFrom('interface A { [Default] object toJSON(); attribute DOMString bar; };').default_to_json_operation();
+ assert_equals(map.size, 1);
+ assert_true(map.has("bar"));
+ assert_equals(map.get("bar").idlType, "DOMString");
+ }, 'should return a map whose key/value pair represent the identifier and IDL type of valid attributes');
+
+ test(function() {
+ var context = new IdlArray();
+ context.add_idls("interface A : B { [Default] object toJSON(); attribute DOMString a; };");
+ context.add_idls("interface B { [Default] object toJSON(); attribute long b; };");
+ var map = context.members.A.default_to_json_operation();
+ assert_array_equals([...map.keys()], ["b", "a"]);
+ assert_array_equals([...map.values()].map(v => v.idlType), ["long", "DOMString"]);
+ }, 'should return a properly ordered map that contains IDL types of valid attributes for inherited interfaces');
+
+ test(function() {
+ var context = new IdlArray();
+ context.add_idls("interface A : B { attribute DOMString a; };");
+ context.add_idls("interface B { [Default] object toJSON(); attribute long b; };");
+ var map = context.members.A.default_to_json_operation();
+ assert_equals(map.size, 1);
+ assert_true(map.has("b"));
+ assert_equals(map.get("b").idlType, "long");
+ assert_array_equals([...map.keys()], ["b"]);
+ }, 'should not include attributes of the current interface when the [Default] toJSON method in inherited');
+
+ test(function() {
+ var context = new IdlArray();
+ context.add_idls("interface A : B { [Default] object toJSON(); };");
+ context.add_idls("interface B : C { [Default] object toJSON(); attribute DOMString foo; };");
+ context.add_idls("interface C { [Default] object toJSON(); attribute long foo; };");
+ var map = context.members.A.default_to_json_operation();
+ assert_equals(map.size, 1);
+ assert_true(map.has("foo"));
+ assert_equals(map.get("foo").idlType, "DOMString");
+ }, 'attributes declared further away in the inheritance hierarchy should be masked by attributes declared closer');
+
+ test(function() {
+ var context = new IdlArray();
+ context.add_idls("interface A { [Default] object toJSON(); attribute DOMString a; };");
+ context.add_idls("interface B : A { attribute any b; };");
+ context.add_idls("interface C : B { [Default] object toJSON(); attribute long c; };");
+ var map = context.members.C.default_to_json_operation();
+ assert_array_equals([...map.keys()], ["a", "c"]);
+ assert_array_equals([...map.values()].map(v => v.idlType), ["DOMString", "long"]);
+ }, 'should return an ordered map that ignores attributes of inherited interfaces which do not declare a [Default] toJSON operation.');
+
+ test(function() {
+ var context = new IdlArray();
+ context.add_idls("interface D { attribute DOMString d; };");
+ context.add_idls("interface mixin M { [Default] object toJSON(); attribute long m; };");
+ context.add_idls("D includes M;");
+ context.merge_mixins();
+ var map = context.members.D.default_to_json_operation();
+ assert_array_equals([...map.keys()], ["d", "m"]);
+ assert_array_equals([...map.values()].map(v => v.idlType), ["DOMString", "long"]);
+ }, 'should return a properly ordered map that accounts for mixed-in interfaces which declare a [Default] toJSON operation.');
+</script>
+</body>
+</html>
+
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/do_member_unscopable_asserts.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/do_member_unscopable_asserts.html
new file mode 100644
index 0000000000..90142efe6b
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/do_member_unscopable_asserts.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>IdlDictionary.prototype.do_member_unscopable_asserts()</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+ 'use strict';
+ function mock_interface_A(unscopables) {
+ self.A = function A() {};
+ A.prototype[Symbol.unscopables] = unscopables;
+ }
+
+ test(function() {
+ const i = interfaceFrom('interface A { [Unscopable] attribute any x; };');
+ const member = i.members[0];
+ assert_true(member.isUnscopable);
+ mock_interface_A({ x: true });
+ i.do_member_unscopable_asserts(member);
+ }, 'should not throw for [Unscopable] with property in @@unscopables');
+
+ test(function() {
+ const i = interfaceFrom('interface A { [Unscopable] attribute any x; };');
+ const member = i.members[0];
+ assert_true(member.isUnscopable);
+ mock_interface_A({});
+ // assert_throws_* can't be used because they rethrow AssertionErrors.
+ try {
+ i.do_member_unscopable_asserts(member);
+ } catch(e) {
+ assert_true(e.message.includes('Symbol.unscopables'));
+ return;
+ }
+ assert_unreached('did not throw');
+ }, 'should throw for [Unscopable] with property missing from @@unscopables');
+
+ // This test checks that for attributes/methods which aren't [Unscopable]
+ // in the IDL, we don't assert that @@unscopables is missing the property.
+ // This could miss implementation bugs, but [Unscopable] is so rarely used
+ // that it's fairly unlikely to ever happen.
+ test(function() {
+ const i = interfaceFrom('interface A { attribute any x; };');
+ const member = i.members[0];
+ assert_false(member.isUnscopable);
+ mock_interface_A({ x: true });
+ i.do_member_unscopable_asserts(member);
+ }, 'should not throw if [Unscopable] is used but property is in @@unscopables');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_interface_object.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_interface_object.html
new file mode 100644
index 0000000000..a3d901a752
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_interface_object.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<title>IdlInterface.prototype.get_interface_object()</title>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+"use strict";
+test(function() {
+ window.A = {};
+ var i = interfaceFrom('interface A { };');
+ assert_equals(i.get_interface_object(), window.A);
+}, 'Interface does not have LegacyNamespace.');
+
+test(function() {
+ window.Foo = { A: {} };
+ var i = interfaceFrom('[LegacyNamespace=Foo] interface A { }; namespace Foo { };');
+ assert_equals(i.get_interface_object(), window.Foo.A);
+}, 'Interface has LegacyNamespace');
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_interface_object_owner.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_interface_object_owner.html
new file mode 100644
index 0000000000..51ab2067bc
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_interface_object_owner.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<title>IdlInterface.prototype.get_interface_object_owner()</title>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+"use strict";
+test(function() {
+ var i = interfaceFrom('interface A { };');
+ assert_equals(i.get_interface_object_owner(), window);
+}, 'Interface does not have LegacyNamespace.');
+
+test(function() {
+ window.Foo = {};
+ var i = interfaceFrom('[LegacyNamespace=Foo] interface A { }; namespace Foo { };');
+ assert_equals(i.get_interface_object_owner(), window.Foo);
+}, 'Interface has LegacyNamespace');
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_legacy_namespace.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_legacy_namespace.html
new file mode 100644
index 0000000000..e2d42bb09e
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_legacy_namespace.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<title>IdlInterface.prototype.get_legacy_namespace()</title>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+"use strict";
+test(function() {
+ var i = interfaceFrom('interface A { };');
+ assert_equals(i.get_legacy_namespace(), undefined);
+}, 'Interface does not have LegacyNamespace.');
+
+test(function() {
+ var i = interfaceFrom('[LegacyNamespace=Foo] interface A { }; namespace Foo { };');
+ assert_equals(i.get_legacy_namespace(), "Foo");
+}, 'Interface has LegacyNamespace');
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_qualified_name.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_qualified_name.html
new file mode 100644
index 0000000000..677a31b5e7
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_qualified_name.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<title>IdlInterface.prototype.get_qualified_name()</title>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+"use strict";
+test(function() {
+ var i = interfaceFrom('interface A { };');
+ assert_equals(i.get_qualified_name(), "A");
+}, 'Interface does not have LegacyNamespace.');
+
+test(function() {
+ var i = interfaceFrom('[LegacyNamespace=Foo] interface A { }; namespace Foo { };');
+ assert_equals(i.get_qualified_name(), "Foo.A");
+}, 'Interface has LegacyNamespace');
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_reverse_inheritance_stack.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_reverse_inheritance_stack.html
new file mode 100644
index 0000000000..0c066baabb
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_reverse_inheritance_stack.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>IdlInterface.prototype.get_reverse_inheritance_stack()</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+ "use strict";
+ test(function() {
+ var stack = interfaceFrom('interface A { };').get_reverse_inheritance_stack();
+ assert_array_equals(stack.map(i => i.name), ["A"]);
+ }, 'should return an array that includes itself.');
+
+ test(function() {
+ var i = interfaceFrom('interface A : B { };');
+ assert_throws_js(Error, _ => i.get_reverse_inheritance_stack());
+ }, "should throw for interfaces which inherit from another interface which wasn't added to the IdlArray");
+
+ test(function() {
+ var idl = new IdlArray();
+ idl.add_idls('interface A : B { };');
+ idl.add_untested_idls('interface B : C { }; interface C { };');
+ var A = idl.members["A"];
+ assert_array_equals(A.get_reverse_inheritance_stack().map(i => i.name), ["C", "B", "A"]);
+ }, 'should return an array of interfaces in order of inheritance, starting with the base interface');
+
+ test(function () {
+ var idl = new IdlArray();
+ idl.add_untested_idls('interface A : B { };');
+ idl.add_untested_idls('interface B : A { };');
+ idl.assert_throws('A has a circular dependency: A,B,A', i => i.test());
+ }, 'should throw when inheritance is circular');
+
+ test(function () {
+ var idl = new IdlArray();
+ idl.add_untested_idls('interface A : B { };');
+ idl.assert_throws(
+ 'Duplicate identifier A',
+ i => i.add_untested_idls('interface A : C { };'));
+ }, 'should throw when multiple inheritances defined');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/has_default_to_json_regular_operation.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/has_default_to_json_regular_operation.html
new file mode 100644
index 0000000000..b47262b72b
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/has_default_to_json_regular_operation.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>IdlInterface.prototype.has_default_to_json_regular_operation()</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+ "use strict";
+ test(function() {
+ var i = interfaceFrom('interface A { };');
+ assert_false(i.has_default_to_json_regular_operation());
+ }, 'should return false when the interface declares no toJSON operation.');
+
+ test(function() {
+ var i = interfaceFrom('interface A { static object toJSON(); };');
+ assert_false(i.has_default_to_json_regular_operation());
+ }, 'should return false when the interface declares a static toJSON operation.');
+
+ test(function() {
+ var i = interfaceFrom('interface A { object toJSON(); };');
+ assert_false(i.has_default_to_json_regular_operation());
+ }, 'should return false when the interface declares a regular toJSON operation with no extended attribute.');
+
+ test(function() {
+ var i = interfaceFrom('interface A { [x] object toJSON(); };');
+ assert_false(i.has_default_to_json_regular_operation());
+ }, 'should return false when the interface declares a regular toJSON operation with another extented attribute.');
+
+ test(function() {
+ var i = interfaceFrom('interface A { [Default] object toJSON(); };');
+ assert_true(i.has_default_to_json_regular_operation());
+ }, 'should return true when the interface declares a regular toJSON operation with the [Default] extented attribute.');
+
+ test(function() {
+ var i = interfaceFrom('interface A { [Attr, AnotherAttr, Default] object toJSON(); };');
+ assert_true(i.has_default_to_json_regular_operation());
+ }, 'should return true when the interface declares a regular toJSON operation with multiple extended attributes, including [Default].');
+</script>
+</body>
+</html>
+
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/has_to_json_regular_operation.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/has_to_json_regular_operation.html
new file mode 100644
index 0000000000..a1a641bd97
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/has_to_json_regular_operation.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>IdlInterface.prototype.has_to_json_regular_operation()</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+ "use strict";
+ test(function() {
+ var i = interfaceFrom('interface A { };');
+ assert_false(i.has_to_json_regular_operation());
+ }, 'should return false when the interface declares no toJSON operation.');
+
+ test(function() {
+ var i = interfaceFrom('interface A { static object toJSON(); };');
+ assert_false(i.has_to_json_regular_operation());
+ }, 'should return false when the interface declares a static toJSON operation.');
+
+ test(function() {
+ var i = interfaceFrom('interface A { object toJSON(); };');
+ assert_true(i.has_to_json_regular_operation());
+ }, 'should return true when the interface declares a regular toJSON operation.');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/should_have_interface_object.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/should_have_interface_object.html
new file mode 100644
index 0000000000..3ce945751d
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/should_have_interface_object.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<title>IdlInterface.prototype.should_have_interface_object()</title>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+"use strict";
+test(function() {
+ var i = interfaceFrom("callback interface A { const unsigned short B = 0; };");
+ assert_true(i.should_have_interface_object());
+}, "callback interface with a constant");
+
+test(function() {
+ var i = interfaceFrom("callback interface A { undefined b(); sequence<any> c(); };");
+ assert_false(i.should_have_interface_object());
+}, "callback interface without a constant");
+
+test(function() {
+ var i = interfaceFrom("[LegacyNoInterfaceObject] interface A { };");
+ assert_false(i.should_have_interface_object());
+}, "non-callback interface with [LegacyNoInterfaceObject]");
+
+test(function() {
+ var i = interfaceFrom("interface A { };");
+ assert_true(i.should_have_interface_object());
+}, "non-callback interface without [LegacyNoInterfaceObject]");
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/test_primary_interface_of_undefined.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/test_primary_interface_of_undefined.html
new file mode 100644
index 0000000000..0031558ad4
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/test_primary_interface_of_undefined.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <title>idlharness test_primary_interface_of_undefined</title>
+</head>
+
+<body>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/WebIDLParser.js"></script>
+ <script src="/resources/idlharness.js"></script>
+ <script>
+ 'use strict';
+ test(function () {
+ let i = new IdlArray();
+ i.add_untested_idls('interface A : B {};');
+ i.assert_throws(new IdlHarnessError('A inherits B, but B is undefined.'), i => i.test());
+ }, 'A : B with B undeclared should throw IdlHarnessError');
+
+ test(function () {
+ let i = new IdlArray();
+ i.add_untested_idls('interface A : B {};');
+ i.add_untested_idls('dictionary B {};');
+ i.assert_throws(new IdlHarnessError('A inherits B, but B is not an interface.'), i => i.test());
+ }, 'interface A : B with B dictionary should throw IdlHarnessError');
+ </script>
+</body>
+
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterfaceMember/is_to_json_regular_operation.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterfaceMember/is_to_json_regular_operation.html
new file mode 100644
index 0000000000..b3f402dd08
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterfaceMember/is_to_json_regular_operation.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>IdlInterfaceMember.prototype.is_to_json_regular_operation()</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+ "use strict";
+ test(function() {
+ var m = memberFrom("readonly attribute DOMString foo");
+ assert_false(m.is_to_json_regular_operation());
+ }, 'should return false when member is an attribute.');
+
+ test(function() {
+ var m = memberFrom("static undefined foo()");
+ assert_false(m.is_to_json_regular_operation());
+ }, 'should return false when member is a static operation.');
+
+ test(function() {
+ var m = memberFrom("static object toJSON()");
+ assert_false(m.is_to_json_regular_operation());
+ }, 'should return false when member is a static toJSON operation.');
+
+ test(function() {
+ var m = memberFrom("object toJSON()");
+ assert_true(m.is_to_json_regular_operation());
+ }, 'should return true when member is a regular toJSON operation.');
+
+ test(function() {
+ var m = memberFrom("[Foo] object toJSON()");
+ assert_true(m.is_to_json_regular_operation());
+ }, 'should return true when member is a regular toJSON operation with extensible attributes.');
+</script>
+</body>
+</html>
+
diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterfaceMember/toString.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterfaceMember/toString.html
new file mode 100644
index 0000000000..054dbb1ccb
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterfaceMember/toString.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>IdlInterfaceMember.prototype.toString()</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script src="../../../idl-helper.js"></script>
+<script>
+"use strict";
+const tests = [
+ ["long x", "long"],
+ ["long? x", "long?"],
+ ["Promise<long> x", "Promise<long>"],
+ ["Promise<long?> x", "Promise<long?>"],
+ ["sequence<long> x", "sequence<long>"],
+ ["(long or DOMString) x", "(long or DOMString)"],
+ ["long x, boolean y", "long, boolean"],
+ ["long x, optional boolean y", "long, optional boolean"],
+ ["long... args", "long..."],
+ ["sequence<long>... args", "sequence<long>..."],
+ ["(long or DOMString)... args", "(long or DOMString)..."],
+];
+for (const [input, output] of tests) {
+ test(function() {
+ var m = memberFrom(`undefined foo(${input})`);
+ assert_equals(m.toString(), `foo(${output})`);
+ }, `toString for ${input}`);
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/assert_implements.html b/testing/web-platform/tests/resources/test/tests/unit/assert_implements.html
new file mode 100644
index 0000000000..6e35f38502
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/assert_implements.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/test/tests/unit/helpers.js"></script>
+<title>assert_implements unittests</title>
+<script>
+'use strict';
+
+test(() => {
+ // All values in JS that are not falsy are truthy, so we just check some
+ // common cases here.
+ assert_implements(true, 'true is a truthy value');
+ assert_implements(5, 'positive integeter is a truthy value');
+ assert_implements(-5, 'negative integeter is a truthy value');
+ assert_implements('foo', 'non-empty string is a truthy value');
+}, 'truthy values');
+
+test_failure(() => {
+ assert_implements(false);
+}, 'false is a falsy value');
+
+test_failure(() => {
+ assert_implements(0);
+}, '0 is a falsy value');
+
+test_failure(() => {
+ assert_implements('');
+}, 'empty string is a falsy value');
+
+test_failure(() => {
+ assert_implements(null);
+}, 'null is a falsy value');
+
+test_failure(() => {
+ assert_implements(undefined);
+}, 'undefined is a falsy value');
+
+test_failure(() => {
+ assert_implements(NaN);
+}, 'NaN is a falsy value');
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/assert_implements_optional.html b/testing/web-platform/tests/resources/test/tests/unit/assert_implements_optional.html
new file mode 100644
index 0000000000..4f23e203c5
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/assert_implements_optional.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/test/tests/unit/helpers.js"></script>
+<title>assert_implements_optional unittests</title>
+<script>
+'use strict';
+
+test(() => {
+ // All values in JS that are not falsy are truthy, so we just check some
+ // common cases here.
+ assert_implements_optional(true, 'true is a truthy value');
+ assert_implements_optional(5, 'positive integeter is a truthy value');
+ assert_implements_optional(-5, 'negative integeter is a truthy value');
+ assert_implements_optional('foo', 'non-empty string is a truthy value');
+}, 'truthy values');
+
+test_failure(() => {
+ assert_implements_optional(false);
+}, 'false is a falsy value');
+
+test_failure(() => {
+ assert_implements_optional(0);
+}, '0 is a falsy value');
+
+test_failure(() => {
+ assert_implements_optional('');
+}, 'empty string is a falsy value');
+
+test_failure(() => {
+ assert_implements_optional(null);
+}, 'null is a falsy value');
+
+test_failure(() => {
+ assert_implements_optional(undefined);
+}, 'undefined is a falsy value');
+
+test_failure(() => {
+ assert_implements_optional(NaN);
+}, 'NaN is a falsy value');
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/assert_object_equals.html b/testing/web-platform/tests/resources/test/tests/unit/assert_object_equals.html
new file mode 100644
index 0000000000..313d77b977
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/assert_object_equals.html
@@ -0,0 +1,152 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/test/tests/unit/helpers.js"></script>
+<title>Assertion functions</title>
+<script>
+'use strict';
+
+test(function() {
+ assert_object_equals({}, {});
+}, 'empty objects');
+
+test(function() {
+ var actual = {};
+ var expected = {};
+ actual.a = actual;
+ expected.a = actual;
+
+ assert_object_equals(actual, expected);
+}, 'tolerates cycles in actual value');
+
+test(function() {
+ var actual = {};
+ var expected = {};
+ actual.a = expected;
+ expected.a = expected;
+
+ assert_object_equals(actual, expected);
+}, 'tolerates cycles in expected value');
+
+test(function() {
+ assert_object_equals({2: 99, 0: 23, 1: 45}, [23, 45, 99]);
+}, 'recognizes equivalence of actual object and expected array');
+
+test(function() {
+ assert_object_equals([23, 45, 99], {2: 99, 0: 23, 1: 45});
+}, 'recognizes equivalence of actual array and expected object');
+
+test(function() {
+ var actual = {};
+ var expected = {};
+ Object.defineProperty(actual, 'a', { value: 1, enumerable: false });
+
+ assert_not_equals(actual.a, expected.a);
+ assert_object_equals(actual, expected);
+}, 'non-enumerable properties in actual value ignored');
+
+test(function() {
+ var actual = {};
+ var expected = {};
+ Object.defineProperty(expected, 'a', { value: 1, enumerable: false });
+
+ assert_not_equals(actual.a, expected.a);
+ assert_object_equals(actual, expected);
+}, 'non-enumerable properties in expected value ignored');
+
+test(function() {
+ assert_object_equals({c: 3, a: 1, b: 2}, {a: 1, b: 2, c: 3});
+}, 'equivalent objects - "flat" object');
+
+test(function() {
+ assert_object_equals(
+ {c: {e: 5, d: 4}, b: 2, a: 1},
+ {a: 1, b: 2, c: {d: 4, e: 5}}
+ );
+}, 'equivalent objects - nested object');
+
+test(function() {
+ assert_object_equals(
+ {c: [4, 5], b: 2, a: 1},
+ {a: 1, b: 2, c: [4, 5]}
+ );
+}, 'equivalent objects - nested array');
+
+test(function() {
+ assert_object_equals({a: NaN}, {a: NaN});
+}, 'equivalent objects - NaN value');
+
+test(function() {
+ assert_object_equals({a: -0}, {a: -0});
+}, 'equivalent objects - negative zero value');
+
+test_failure(function() {
+ assert_object_equals(undefined, {});
+}, 'invalid actual value: undefined');
+
+test_failure(function() {
+ assert_object_equals(null, {});
+}, 'invalid actual value: null');
+
+test_failure(function() {
+ assert_object_equals(34, {});
+}, 'invalid actual value: number');
+
+test_failure(function() {
+ assert_object_equals({c: 3, a: 1, b: 2}, {a: 1, b: 1, c: 3});
+}, 'unequal property value - "flat" object');
+
+test_failure(function() {
+ var actual = Object.create({a: undefined});
+ var expected = {};
+
+ assert_object_equals(actual, expected);
+}, 'non-own properties in actual value verified');
+
+test_failure(function() {
+ var actual = {};
+ var expected = Object.create({a: undefined});
+
+ assert_object_equals(actual, expected);
+}, 'non-own properties in expected value verified');
+
+test_failure(function() {
+ assert_object_equals(
+ {a: 1, b: 2, c: {d: 5, e: 5, f: 6}},
+ {a: 1, b: 2, c: {d: 5, e: 7, f: 6}}
+ );
+}, 'unequal property value - nested object');
+
+test_failure(function() {
+ assert_object_equals(
+ {a: 1, b: 2, c: [4, 5, 6]},
+ {a: 1, b: 2, c: [4, 7, 6]}
+ );
+}, 'unequal property value - nested array');
+
+test_failure(function() {
+ assert_object_equals({a: NaN}, {a: 0});
+}, 'equivalent objects - NaN actual value');
+
+test_failure(function() {
+ assert_object_equals({a: 0}, {a: NaN});
+}, 'equivalent objects - NaN expected value');
+
+test_failure(function() {
+ assert_object_equals({a: -0}, {a: 0});
+}, 'equivalent objects - negative zero actual value');
+
+test_failure(function() {
+ assert_object_equals({a: 0}, {a: -0});
+}, 'equivalent objects - negative zero expected value');
+
+test_failure(function() {
+ assert_object_equals({a: 1}, {});
+}, 'actual contains additional property');
+
+test_failure(function() {
+ assert_object_equals({}, {a: 1});
+}, 'expected contains additional property');
+</script>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/async-test-return-restrictions.html b/testing/web-platform/tests/resources/test/tests/unit/async-test-return-restrictions.html
new file mode 100644
index 0000000000..0fde2e2422
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/async-test-return-restrictions.html
@@ -0,0 +1,135 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="/resources/testharness.js"></script>
+ <title>Restrictions on return value from `async_test`</title>
+</head>
+<body>
+<script>
+function makeTest(...bodies) {
+ const closeScript = '<' + '/script>';
+ let src = `
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Document title</title>
+<script src="/resources/testharness.js?${Math.random()}">${closeScript}
+</head>
+
+<body>
+<div id="log"></div>`;
+ bodies.forEach((body) => {
+ src += '<script>(' + body + ')();' + closeScript;
+ });
+
+ const iframe = document.createElement('iframe');
+
+ document.body.appendChild(iframe);
+ iframe.contentDocument.write(src);
+
+ return new Promise((resolve) => {
+ window.addEventListener('message', function onMessage(e) {
+ if (e.source !== iframe.contentWindow) {
+ return;
+ }
+ if (!e.data || e.data.type !=='complete') {
+ return;
+ }
+ window.removeEventListener('message', onMessage);
+ resolve(e.data);
+ });
+
+ iframe.contentDocument.close();
+ }).then(({ tests, status }) => {
+ const summary = {
+ harness: {
+ status: getEnumProp(status, status.status),
+ message: status.message
+ },
+ tests: {}
+ };
+
+ tests.forEach((test) => {
+ summary.tests[test.name] = getEnumProp(test, test.status);
+ });
+
+ return summary;
+ });
+}
+
+function getEnumProp(object, value) {
+ for (let property in object) {
+ if (!/^[A-Z]+$/.test(property)) {
+ continue;
+ }
+
+ if (object[property] === value) {
+ return property;
+ }
+ }
+}
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ async_test((t) => {t.done(); return undefined;}, 'before');
+ async_test((t) => {t.done(); return null;}, 'null');
+ async_test((t) => {t.done(); return undefined;}, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'ERROR');
+ assert_equals(
+ harness.message,
+ 'Test named "null" passed a function to `async_test` that returned a value.'
+ );
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.null, 'PASS');
+ // This test did not get the chance to start.
+ assert_equals(tests.after, undefined);
+ });
+}, 'test returning `null`');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ async_test((t) => {t.done(); return undefined;}, 'before');
+ async_test((t) => {t.done(); return {};}, 'object');
+ async_test((t) => {t.done(); return undefined;}, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'ERROR');
+ assert_equals(
+ harness.message,
+ 'Test named "object" passed a function to `async_test` that returned a value.'
+ );
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.object, 'PASS');
+ // This test did not get the chance to start.
+ assert_equals(tests.after, undefined);
+ });
+}, 'test returning an ordinary object');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ async_test((t) => {t.done(); return undefined;}, 'before');
+ async_test((t) => {t.done(); return Promise.resolve(5);}, 'thenable');
+ async_test((t) => {t.done(); return undefined;}, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'ERROR');
+ assert_equals(
+ harness.message,
+ 'Test named "thenable" passed a function to `async_test` that returned a value. ' +
+ 'Consider using `promise_test` instead when using Promises or async/await.'
+ );
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.thenable, 'PASS');
+ // This test did not get a chance to start.
+ assert_equals(tests.after, undefined);
+ });
+}, 'test returning a thenable object');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/basic.html b/testing/web-platform/tests/resources/test/tests/unit/basic.html
new file mode 100644
index 0000000000..d52082f2e0
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/basic.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>idlharness basic</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/WebIDLParser.js"></script>
+<script src="/resources/idlharness.js"></script>
+<script>
+ "use strict";
+ test(function() {
+ assert_true("IdlArray" in window);
+ }, 'IdlArray constructor should be a global object');
+ test(function() {
+ assert_true(new IdlArray() instanceof IdlArray);
+ }, 'IdlArray constructor should be constructible');
+ test(function() {
+ assert_true("WebIDL2" in window);
+ }, 'WebIDL2 namespace should be a global object');
+ test(function() {
+ assert_equals(typeof WebIDL2.parse, "function");
+ }, 'WebIDL2 namespace should have a parse method');
+ test(function() {
+ try {
+ WebIDL2.parse("I'm a syntax error");
+ throw new Error("Web IDL didn't throw");
+ } catch (e) {
+ assert_equals(e.name, "WebIDLParseError");
+ }
+ }, 'WebIDL2 parse method should bail on incorrect WebIDL');
+ test(function() {
+ assert_equals(typeof WebIDL2.parse("interface Foo {};"), "object");
+ }, 'WebIDL2 parse method should produce an AST for correct WebIDL');
+ test(function () {
+ try {
+ let i = new IdlArray();
+ i.add_untested_idls(`interface C {};`);
+ i.assert_throws('Anything', i => i.test());
+ } catch (e) {
+ assert_true(e instanceof IdlHarnessError);
+ }
+ }, `assert_throws should throw if no IdlHarnessError thrown`);
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/exceptional-cases-timeouts.html b/testing/web-platform/tests/resources/test/tests/unit/exceptional-cases-timeouts.html
new file mode 100644
index 0000000000..760ac7154f
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/exceptional-cases-timeouts.html
@@ -0,0 +1,120 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <script src="/resources/testharness.js"></script>
+ <title>Exceptional cases - timeouts</title>
+</head>
+<body>
+<p>
+ The tests in this file are executed in parallel to avoid exceeding the "long"
+ timeout duration.
+</p>
+<script>
+function makeTest(...bodies) {
+ const closeScript = '<' + '/script>';
+ let src = `
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Document title</title>
+<script src="/resources/testharness.js?${Math.random()}">${closeScript}
+</head>
+
+<body>
+<div id="log"></div>`;
+ bodies.forEach((body) => {
+ src += '<script>(' + body + ')();' + closeScript;
+ });
+
+ const iframe = document.createElement('iframe');
+
+ document.body.appendChild(iframe);
+ iframe.contentDocument.write(src);
+
+ return new Promise((resolve) => {
+ window.addEventListener('message', function onMessage(e) {
+ if (e.source !== iframe.contentWindow) {
+ return;
+ }
+ if (!e.data || e.data.type !=='complete') {
+ return;
+ }
+ window.removeEventListener('message', onMessage);
+ resolve(e.data);
+ });
+
+ iframe.contentDocument.close();
+ }).then(({ tests, status }) => {
+ const summary = {
+ harness: getEnumProp(status, status.status),
+ tests: {}
+ };
+
+ tests.forEach((test) => {
+ summary.tests[test.name] = getEnumProp(test, test.status);
+ });
+
+ return summary;
+ });
+}
+
+function getEnumProp(object, value) {
+ for (let property in object) {
+ if (!/^[A-Z]+$/.test(property)) {
+ continue;
+ }
+
+ if (object[property] === value) {
+ return property;
+ }
+ }
+}
+
+(() => {
+ window.asyncTestCleanupCount1 = 0;
+ const nestedTest = makeTest(
+ () => {
+ async_test((t) => {
+ t.add_cleanup(() => window.parent.asyncTestCleanupCount1 += 1);
+ setTimeout(() => {
+ throw new Error('this error is expected');
+ });
+ }, 'test');
+ }
+ );
+ promise_test(() => {
+ return nestedTest.then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.test, 'TIMEOUT');
+ assert_equals(window.asyncTestCleanupCount1, 1);
+ });
+ }, 'uncaught exception during async_test which times out');
+})();
+
+(() => {
+ window.promiseTestCleanupCount2 = 0;
+ const nestedTest = makeTest(
+ () => {
+ promise_test((t) => {
+ t.add_cleanup(() => window.parent.promiseTestCleanupCount2 += 1);
+ setTimeout(() => {
+ throw new Error('this error is expected');
+ });
+
+ return new Promise(() => {});
+ }, 'test');
+ }
+ );
+ promise_test(() => {
+ return nestedTest.then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.test, 'TIMEOUT');
+ assert_equals(window.promiseTestCleanupCount2, 1);
+ });
+ }, 'uncaught exception during promise_test which times out');
+})();
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/exceptional-cases.html b/testing/web-platform/tests/resources/test/tests/unit/exceptional-cases.html
new file mode 100644
index 0000000000..4054d0311d
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/exceptional-cases.html
@@ -0,0 +1,392 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <script src="/resources/testharness.js"></script>
+ <title>Exceptional cases</title>
+</head>
+<body>
+<script>
+function makeTest(...bodies) {
+ const closeScript = '<' + '/script>';
+ let src = `
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Document title</title>
+<script src="/resources/testharness.js?${Math.random()}">${closeScript}
+</head>
+
+<body>
+<div id="log"></div>`;
+ bodies.forEach((body) => {
+ src += '<script>(' + body + ')();' + closeScript;
+ });
+
+ const iframe = document.createElement('iframe');
+
+ document.body.appendChild(iframe);
+ iframe.contentDocument.write(src);
+
+ return new Promise((resolve) => {
+ window.addEventListener('message', function onMessage(e) {
+ if (e.source !== iframe.contentWindow) {
+ return;
+ }
+ if (!e.data || e.data.type !=='complete') {
+ return;
+ }
+ window.removeEventListener('message', onMessage);
+ resolve(e.data);
+ });
+
+ iframe.contentDocument.close();
+ }).then(({ tests, status }) => {
+ const summary = {
+ harness: getEnumProp(status, status.status),
+ tests: {}
+ };
+
+ tests.forEach((test) => {
+ summary.tests[test.name] = getEnumProp(test, test.status);
+ });
+
+ return summary;
+ });
+}
+
+function getEnumProp(object, value) {
+ for (let property in object) {
+ if (!/^[A-Z]+$/.test(property)) {
+ continue;
+ }
+
+ if (object[property] === value) {
+ return property;
+ }
+ }
+}
+
+promise_test(() => {
+ return makeTest(
+ () => { done(); }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_array_equals(Object.keys(tests), []);
+ });
+}, 'completion signaled before testing begins');
+
+promise_test(() => {
+ return makeTest(
+ () => { assert_true(true); done(); }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_array_equals(Object.keys(tests), []);
+ });
+}, 'passing assertion before testing begins');
+
+promise_test(() => {
+ return makeTest(
+ () => { assert_false(true); }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_array_equals(Object.keys(tests), []);
+ });
+}, 'failing assertion before testing begins');
+
+promise_test(() => {
+ return makeTest(
+ () => { throw new Error('this error is expected'); }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_array_equals(Object.keys(tests), []);
+ });
+}, 'uncaught exception before testing begins');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ allow_uncaught_exception: true });
+ throw new Error('this error is expected');
+ },
+ () => {
+ test(function() {}, 'a');
+ test(function() {}, 'b');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests.a, 'PASS');
+ assert_equals(tests.b, 'PASS');
+ });
+}, 'uncaught exception with subsequent subtest');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ async_test((t) => {
+ setTimeout(() => {
+ setTimeout(() => t.done(), 0);
+ async_test((t) => { setTimeout(t.done.bind(t), 0); }, 'after');
+ throw new Error('this error is expected');
+ }, 0);
+ }, 'during');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.during, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'uncaught exception during async_test');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ promise_test(() => {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve();
+ promise_test(() => Promise.resolve(), 'after');
+ throw new Error('this error is expected');
+ }, 0);
+ });
+ }, 'during');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.during, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'uncaught exception during promise_test');
+
+promise_test(() => {
+ return makeTest(
+ () => { test(() => {}, 'before'); },
+ () => { throw new Error('this error is expected'); },
+ () => { test(() => {}, 'after'); }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'uncaught exception between tests');
+
+promise_test(() => {
+ return makeTest(
+ () => { promise_test(() => Promise.resolve(), 'before'); },
+ () => { throw new Error('this error is expected'); },
+ () => { promise_test(() => Promise.resolve(), 'after'); }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'uncaught exception between promise_tests');
+
+
+// This feature of testharness.js is only observable in browsers which
+// implement the `unhandledrejection` event.
+if ('onunhandledrejection' in window) {
+
+ promise_test(() => {
+ return makeTest(
+ () => { Promise.reject(new Error('this error is expected')); }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_array_equals(Object.keys(tests), []);
+ });
+ }, 'unhandled rejection before testing begins');
+
+ promise_test(() => {
+ return makeTest(
+ () => {
+ async_test((t) => {
+ Promise.reject(new Error('this error is expected'));
+
+ window.addEventListener('unhandledrejection', () => {
+ setTimeout(() => t.done(), 0);
+ async_test((t) => { setTimeout(t.done.bind(t), 0); }, 'after');
+ t.done();
+ });
+ }, 'during');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.during, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+ }, 'unhandled rejection during async_test');
+
+ promise_test(() => {
+ return makeTest(
+ () => {
+ promise_test(() => {
+ return new Promise((resolve) => {
+ Promise.reject(new Error('this error is expected'));
+
+ window.addEventListener('unhandledrejection', () => {
+ resolve();
+ promise_test(() => Promise.resolve(), 'after');
+ throw new Error('this error is expected');
+ }, 0);
+ });
+ }, 'during');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.during, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+ }, 'unhandled rejection during promise_test');
+
+ promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ explicit_done: true });
+ test(() => {}, 'before');
+ Promise.reject(new Error('this error is expected'));
+ window.addEventListener('unhandledrejection', () => {
+ test(() => {}, 'after');
+ done();
+ });
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_true('after' in tests);
+ });
+ }, 'unhandled rejection between tests');
+
+ promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ explicit_done: true });
+ async_test((t) => { setTimeout(t.done.bind(t), 0); }, 'before');
+ Promise.reject(new Error('this error is expected'));
+ window.addEventListener('unhandledrejection', () => {
+ async_test((t) => { setTimeout(t.done.bind(t), 0); }, 'after');
+ done();
+ });
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+ }, 'unhandled rejection between async_tests');
+
+ promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ explicit_done: true });
+ promise_test(() => Promise.resolve(), 'before');
+ Promise.reject(new Error('this error is expected'));
+ window.addEventListener('unhandledrejection', () => {
+ promise_test(() => Promise.resolve(), 'after');
+ done();
+ });
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_true('after' in tests);
+ });
+ }, 'unhandled rejection between promise_tests');
+
+ promise_test(() => {
+ return makeTest(
+ () => {
+ test((t) => {
+ t.add_cleanup(() => { throw new Error('this error is expected'); });
+ }, 'during');
+ test((t) => {}, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.during, 'PASS');
+ assert_equals(tests.after, 'NOTRUN');
+ });
+ }, 'exception in `add_cleanup` of a test');
+
+}
+
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ setup({explicit_done: true});
+ window.addEventListener('DOMContentLoaded', () => {
+ async_test((t) => {
+ t.add_cleanup(() => {
+ setTimeout(() => {
+ async_test((t) => t.done(), 'after');
+ done();
+ }, 0);
+ throw new Error('this error is expected');
+ });
+ setTimeout(t.done.bind(t), 0);
+ }, 'during');
+ });
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.during, 'PASS');
+ assert_equals(tests.after, 'NOTRUN');
+ });
+}, 'exception in `add_cleanup` of an async_test');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ promise_test((t) => {
+ t.add_cleanup(() => { throw new Error('this error is expected'); });
+ return Promise.resolve();
+ }, 'test');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.test, 'PASS');
+ });
+}, 'exception in `add_cleanup` of a promise_test');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ promise_test((t) => {
+ t.step(() => {
+ throw new Error('this error is expected');
+ });
+ }, 'test');
+ async_test((t) => t.done(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests.test, 'FAIL');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'exception in `step` of an async_test');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ promise_test((t) => {
+ t.step(() => {
+ throw new Error('this error is expected');
+ });
+
+ return new Promise(() => {});
+ }, 'test');
+
+ // This following test should be run to completion despite the fact
+ // that the promise returned by the previous test never resolves.
+ promise_test((t) => Promise.resolve(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests.test, 'FAIL');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'exception in `step` of a promise_test');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/format-value.html b/testing/web-platform/tests/resources/test/tests/unit/format-value.html
new file mode 100644
index 0000000000..13d01b81f3
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/format-value.html
@@ -0,0 +1,123 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>format_value utility function</title>
+ <meta charset="utf-8">
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+"use strict";
+
+test(function() {
+ assert_equals(format_value(null), "null");
+}, "null");
+
+test(function() {
+ assert_equals(format_value(undefined), "undefined");
+}, "undefined");
+
+test(function() {
+ assert_equals(format_value(true), "true");
+ assert_equals(format_value(false), "false");
+}, "boolean values");
+
+test(function() {
+ assert_equals(format_value(0.4), "0.4");
+ assert_equals(format_value(0), "0");
+ assert_equals(format_value(-0), "-0");
+}, "number values");
+
+test(function() {
+ assert_equals(format_value("a string"), "\"a string\"");
+ assert_equals(format_value("new\nline"), "\"new\\nline\"");
+}, "string values");
+
+test(function() {
+ var node = document.createElement("span");
+ node.setAttribute("data-foo", "bar");
+ assert_true(
+ /<span\b/i.test(format_value(node)), "element includes tag name"
+ );
+ assert_true(
+ /data-foo=["']?bar["']?/i.test(format_value(node)),
+ "element includes attributes"
+ );
+}, "node value: element node");
+
+test(function() {
+ var text = document.createTextNode("wpt");
+ assert_equals(format_value(text), "Text node \"wpt\"");
+}, "node value: text node");
+
+test(function() {
+ var node = document.createProcessingInstruction("wpt1", "wpt2");
+ assert_equals(
+ format_value(node),
+ "ProcessingInstruction node with target \"wpt1\" and data \"wpt2\""
+ );
+}, "node value: ProcessingInstruction node");
+
+test(function() {
+ var node = document.createComment("wpt");
+ assert_equals(format_value(node), "Comment node <!--wpt-->");
+}, "node value: comment node");
+
+test(function() {
+ var node = document.implementation.createDocument(
+ "application/xhtml+xml", "", null
+ );
+
+ assert_equals(format_value(node), "Document node with 0 children");
+
+ node.appendChild(document.createElement('html'));
+
+ assert_equals(format_value(node), "Document node with 1 child");
+}, "node value: document node");
+
+test(function() {
+ var node = document.implementation.createDocumentType("foo", "baz", "baz");
+
+ assert_equals(format_value(node), "DocumentType node");
+}, "node value: DocumentType node");
+
+test(function() {
+ var node = document.createDocumentFragment();
+
+ assert_equals(format_value(node), "DocumentFragment node with 0 children");
+
+ node.appendChild(document.createElement("span"));
+
+ assert_equals(format_value(node), "DocumentFragment node with 1 child");
+
+ node.appendChild(document.createElement("span"));
+
+ assert_equals(format_value(node), "DocumentFragment node with 2 children");
+}, "node value: DocumentFragment node");
+
+test(function() {
+ assert_equals(format_value(Symbol("wpt")), "symbol \"Symbol(wpt)\"");
+}, "symbol value");
+
+test(function() {
+ assert_equals(format_value([]), "[]");
+ assert_equals(format_value(["one"]), "[\"one\"]");
+ assert_equals(format_value(["one", "two"]), "[\"one\", \"two\"]");
+}, "array values");
+
+test(function() {
+ var obj = {
+ toString: function() {
+ throw "wpt";
+ }
+ };
+
+ assert_equals(
+ format_value(obj), "[stringifying object threw wpt with type string]"
+ );
+}, "object value with faulty `toString`");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/helpers.js b/testing/web-platform/tests/resources/test/tests/unit/helpers.js
new file mode 100644
index 0000000000..ca378a27c9
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/helpers.js
@@ -0,0 +1,21 @@
+// Helper for testing assertion failure cases for a testharness.js API
+//
+// The `assert_throws_*` functions cannot be used for this purpose because they
+// always fail in response to AssertionError exceptions, even when this is
+// expressed as the expected error.
+function test_failure(fn, name) {
+ test(function() {
+ try {
+ fn();
+ } catch (err) {
+ if (err instanceof AssertionError) {
+ return;
+ }
+ throw new AssertionError('Expected an AssertionError, but' + err);
+ }
+ throw new AssertionError(
+ 'Expected an AssertionError, but no error was thrown'
+ );
+ }, name);
+}
+
diff --git a/testing/web-platform/tests/resources/test/tests/unit/late-test.html b/testing/web-platform/tests/resources/test/tests/unit/late-test.html
new file mode 100644
index 0000000000..693d7e3c81
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/late-test.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test declared after harness completion</title>
+</head>
+<body>
+<div id="log"></div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<p>This test simulates an automated test running scenario, where the test
+results emitted by testharness.js may be interpreted after some delay. It is
+intended to demonstrate that in such cases, any additional tests which are
+executed during that delay are <em>not</em> included in the dataset.</p>
+
+<p>Although these "late" tests are likely an indication of a mistake in test
+design, they cannot be detected deterministically, so in the interest of
+stability, they should be silently tolerated.</p>
+<script>
+async_test(function(t) {
+ var source = [
+ "<div id='log'></div>",
+ "<script src='/resources/testharness.js'></" + "script>",
+ "<script src='/resources/testharnessreport.js'></" + "script>",
+ "<script>",
+ "parent.childReady(window);",
+ "setup({ explicit_done: true });",
+ "test(function() {}, 'acceptable test');",
+ "onload = function() {",
+ " done();",
+ " test(function() {}, 'this test is late and should be ignored');",
+ "};",
+ "</" + "script>"
+ ].join("\n");
+ var iframe = document.createElement("iframe");
+
+ document.body.appendChild(iframe);
+ window.childReady = t.step_func(function(childWindow) {
+ childWindow.add_completion_callback(t.step_func(function(tests, status) {
+ t.step_timeout(t.step_func(function() {
+ assert_equals(tests.length, 1);
+ assert_equals(tests[0].name, "acceptable test");
+ assert_equals(status.status, status.OK);
+ t.done();
+ }), 0);
+ }));
+ });
+
+ iframe.contentDocument.open();
+ iframe.contentDocument.write(source);
+ iframe.contentDocument.close();
+});
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/promise_setup-timeout.html b/testing/web-platform/tests/resources/test/tests/unit/promise_setup-timeout.html
new file mode 100644
index 0000000000..c4947feef4
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/promise_setup-timeout.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <script src="/resources/testharness.js"></script>
+ <script src="../../nested-testharness.js"></script>
+ <title>promise_setup - timeout</title>
+</head>
+<body>
+<script>
+'use strict';
+promise_test(() => {
+ return makeTest(
+ () => {
+ test(() => {}, 'before');
+ promise_setup(() => new Promise(() => {}));
+ promise_test(() => Promise.resolve(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'TIMEOUT');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'NOTRUN');
+ });
+}, 'timeout when returned promise does not settle');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/promise_setup.html b/testing/web-platform/tests/resources/test/tests/unit/promise_setup.html
new file mode 100644
index 0000000000..2abb10a476
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/promise_setup.html
@@ -0,0 +1,333 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="/resources/testharness.js"></script>
+ <script src="../../nested-testharness.js"></script>
+ <title>promise_setup</title>
+</head>
+<body>
+<script>
+'use strict';
+promise_test(() => {
+ return makeTest(
+ () => {
+ // Ensure that the harness error is the result of explicit error
+ // handling
+ setup({ allow_uncaught_exception: true });
+
+ test(() => {}, 'before');
+ promise_setup({});
+ promise_test(() => Promise.resolve(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, undefined);
+ });
+}, 'Error when no function provided');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ test(() => {}, 'before');
+ promise_setup(() => Promise.resolve(), {});
+ promise_test(() => Promise.resolve(), 'after');
+ throw new Error('this error is expected');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'Does not apply unspecified configuration properties');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ var properties = {
+ allow_uncaught_exception: true
+ };
+ test(() => {}, 'before');
+ promise_setup(() => Promise.resolve(), properties);
+ promise_test(() => Promise.resolve(), 'after');
+ throw new Error('this error is expected');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'Ignores configuration properties when some tests have already run');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ var properties = {
+ allow_uncaught_exception: true
+ };
+ promise_setup(() => Promise.resolve(), properties);
+ promise_test(() => {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve();
+ throw new Error('this error is expected');
+ });
+ });
+ }, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'Honors configuration properties');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ // Ensure that the harness error is the result of explicit error
+ // handling
+ setup({ allow_uncaught_exception: true });
+
+ test(() => {}, 'before');
+ promise_setup(() => { throw new Error('this error is expected'); });
+ promise_test(() => Promise.resolve(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'NOTRUN');
+ });
+}, 'Error for synchronous exceptions');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ // Ensure that the harness error is the result of explicit error
+ // handling
+ setup({ allow_uncaught_exception: true });
+
+ test(() => {}, 'before');
+ promise_setup(() => undefined);
+ promise_test(() => Promise.resolve(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'NOTRUN');
+ });
+}, 'Error for missing return value');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ // Ensure that the harness error is the result of explicit error
+ // handling
+ setup({ allow_uncaught_exception: true });
+
+ test(() => {}, 'before');
+ var noThen = Promise.resolve();
+ noThen.then = undefined;
+ promise_setup(() => noThen);
+ promise_test(() => Promise.resolve(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'NOTRUN');
+ });
+}, 'Error for non-thenable return value');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ // Ensure that the harness error is the result of explicit error
+ // handling
+ setup({ allow_uncaught_exception: true });
+
+ test(() => {}, 'before');
+ var poisonedThen = {
+ get then() {
+ throw new Error('this error is expected');
+ }
+ };
+ promise_setup(() => poisonedThen);
+ promise_test(() => Promise.resolve(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'NOTRUN');
+ });
+}, 'Error for "poisoned" `then` property');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ // Ensure that the harness error is the result of explicit error
+ // handling
+ setup({ allow_uncaught_exception: true });
+
+ test(() => {}, 'before');
+ var badThen = {
+ then() {
+ throw new Error('this error is expected');
+ }
+ };
+ promise_setup(() => badThen);
+ promise_test(() => Promise.resolve(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'NOTRUN');
+ });
+}, 'Error for synchronous error from `then` method');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ // Ensure that the harness error is the result of explicit error
+ // handling
+ setup({ allow_uncaught_exception: true });
+
+ test(() => {}, 'before');
+ promise_setup(() => Promise.resolve());
+ test(() => {}, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, undefined);
+ });
+}, 'Error for subsequent invocation of `test`');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ // Ensure that the harness error is the result of explicit error
+ // handling
+ setup({ allow_uncaught_exception: true });
+
+ test(() => {}, 'before');
+ promise_setup(() => Promise.resolve());
+ async_test((t) => t.done(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, undefined);
+ });
+}, 'Error for subsequent invocation of `async_test`');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ // Ensure that the harness error is the result of explicit error
+ // handling
+ setup({ allow_uncaught_exception: true });
+
+ test(() => {}, 'before');
+ promise_setup(() => Promise.reject());
+ promise_test(() => Promise.resolve(), 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'NOTRUN');
+ });
+}, 'Error for rejected promise');
+
+promise_test(() => {
+ var expected_sequence = [
+ 'test body',
+ 'promise_setup begin',
+ 'promise_setup end',
+ 'promise_test body'
+ ];
+ var actual_sequence = window.actual_sequence = [];
+
+ return makeTest(
+ () => {
+ test(() => { parent.actual_sequence.push('test body'); }, 'before');
+ promise_setup(() => {
+ parent.actual_sequence.push('promise_setup begin');
+
+ return Promise.resolve()
+ .then(() => new Promise((resolve) => setTimeout(resolve, 300)))
+ .then(() => parent.actual_sequence.push('promise_setup end'));
+ });
+ promise_test(() => {
+ parent.actual_sequence.push('promise_test body');
+ return Promise.resolve();
+ }, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ assert_array_equals(actual_sequence, expected_sequence);
+ });
+}, 'Waits for promise to settle');
+
+promise_test(() => {
+ var expected_sequence = [
+ 'promise_test 1 begin',
+ 'promise_test 1 end',
+ 'promise_setup begin',
+ 'promise_setup end',
+ 'promise_test 2 body'
+ ];
+ var actual_sequence = window.actual_sequence = [];
+
+ return makeTest(
+ () => {
+ promise_test((t) => {
+ parent.actual_sequence.push('promise_test 1 begin');
+
+ return Promise.resolve()
+ .then(() => new Promise((resolve) => t.step_timeout(resolve, 300)))
+ .then(() => parent.actual_sequence.push('promise_test 1 end'));
+ }, 'before');
+ promise_setup(() => {
+ parent.actual_sequence.push('promise_setup begin');
+
+ return Promise.resolve()
+ .then(() => new Promise((resolve) => setTimeout(resolve, 300)))
+ .then(() => parent.actual_sequence.push('promise_setup end'));
+ });
+ promise_test(() => {
+ parent.actual_sequence.push('promise_test 2 body');
+ return Promise.resolve();
+ }, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ assert_array_equals(actual_sequence, expected_sequence);
+ });
+}, 'Waits for existing promise_test to complete');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ var properties = { allow_uncaught_exception: true };
+ promise_test(() => {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ resolve();
+ throw new Error('this error is expected');
+ });
+ });
+ }, 'before');
+ promise_setup(() => Promise.resolve(), properties);
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'ERROR');
+ assert_equals(tests.before, 'PASS');
+ });
+}, 'Defers application of setup properties');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/single_test.html b/testing/web-platform/tests/resources/test/tests/unit/single_test.html
new file mode 100644
index 0000000000..ff766e66ce
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/single_test.html
@@ -0,0 +1,94 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <script src="/resources/testharness.js"></script>
+ <script src="../../nested-testharness.js"></script>
+ <title>single_test</title>
+</head>
+<body>
+<script>
+promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ single_test: true });
+ done();
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'Expected usage');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ single_test: true });
+ throw new Error('this error is expected');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'Uncaught exception');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ single_test: true });
+ Promise.reject(new Error('this error is expected'));
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'Unhandled rejection');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ single_test: true });
+ test(function() {}, 'sync test');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ assert_equals(
+ Object.keys(tests).length, 1, 'no additional subtests created'
+ );
+ });
+}, 'Erroneous usage: subtest declaration (synchronous test)');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ single_test: true });
+ async_test(function(t) { t.done(); }, 'async test');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ assert_equals(
+ Object.keys(tests).length, 1, 'no additional subtests created'
+ );
+ });
+}, 'Erroneous usage: subtest declaration (asynchronous test)');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ setup({ single_test: true });
+ promise_test(function() { return Promise.resolve(); }, 'promise test');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ assert_equals(
+ Object.keys(tests).length, 1, 'no additional subtests created'
+ );
+ });
+}, 'Erroneous usage: subtest declaration (promise test)');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/test-return-restrictions.html b/testing/web-platform/tests/resources/test/tests/unit/test-return-restrictions.html
new file mode 100644
index 0000000000..0295c5214d
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/test-return-restrictions.html
@@ -0,0 +1,156 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="/resources/testharness.js"></script>
+ <title>Restrictions on return value from `test`</title>
+</head>
+<body>
+<script>
+function makeTest(...bodies) {
+ const closeScript = '<' + '/script>';
+ let src = `
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Document title</title>
+<script src="/resources/testharness.js?${Math.random()}">${closeScript}
+</head>
+
+<body>
+<div id="log"></div>`;
+ bodies.forEach((body) => {
+ src += '<script>(' + body + ')();' + closeScript;
+ });
+
+ const iframe = document.createElement('iframe');
+
+ document.body.appendChild(iframe);
+ iframe.contentDocument.write(src);
+
+ return new Promise((resolve) => {
+ window.addEventListener('message', function onMessage(e) {
+ if (e.source !== iframe.contentWindow) {
+ return;
+ }
+ if (!e.data || e.data.type !=='complete') {
+ return;
+ }
+ window.removeEventListener('message', onMessage);
+ resolve(e.data);
+ });
+
+ iframe.contentDocument.close();
+ }).then(({ tests, status }) => {
+ const summary = {
+ harness: {
+ status: getEnumProp(status, status.status),
+ message: status.message
+ },
+ tests: {}
+ };
+
+ tests.forEach((test) => {
+ summary.tests[test.name] = getEnumProp(test, test.status);
+ });
+
+ return summary;
+ });
+}
+
+function getEnumProp(object, value) {
+ for (let property in object) {
+ if (!/^[A-Z]+$/.test(property)) {
+ continue;
+ }
+
+ if (object[property] === value) {
+ return property;
+ }
+ }
+}
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ test(() => undefined, 'before');
+ test(() => null, 'null');
+ test(() => undefined, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'ERROR');
+ assert_equals(
+ harness.message,
+ 'Test named "null" passed a function to `test` that returned a value.'
+ );
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.null, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'test returning `null`');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ test(() => undefined, 'before');
+ test(() => ({}), 'object');
+ test(() => undefined, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'ERROR');
+ assert_equals(
+ harness.message,
+ 'Test named "object" passed a function to `test` that returned a value.'
+ );
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.object, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'test returning an ordinary object');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ test(() => undefined, 'before');
+ test(() => Promise.resolve(5), 'thenable');
+ test(() => undefined, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'ERROR');
+ assert_equals(
+ harness.message,
+ 'Test named "thenable" passed a function to `test` that returned a value. ' +
+ 'Consider using `promise_test` instead when using Promises or async/await.'
+ );
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.thenable, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'test returning a thenable object');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ test(() => undefined, 'before');
+ test(() => {
+ const iframe = document.createElement('iframe');
+ iframe.setAttribute('sandbox', '');
+ document.body.appendChild(iframe);
+ return iframe.contentWindow;
+ }, 'restricted');
+ test(() => undefined, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'ERROR');
+ assert_equals(
+ harness.message,
+ 'Test named "restricted" passed a function to `test` that returned a value.'
+ );
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests.restricted, 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'test returning a restricted object');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/throwing-assertions.html b/testing/web-platform/tests/resources/test/tests/unit/throwing-assertions.html
new file mode 100644
index 0000000000..a36a56043c
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/throwing-assertions.html
@@ -0,0 +1,268 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta name="timeout" content="long">
+ <script src="/resources/testharness.js"></script>
+ <title>Test the methods that make assertions about exceptions</title>
+</head>
+<body>
+<script>
+function makeTest(...bodies) {
+ const closeScript = '<' + '/script>';
+ let src = `
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Document title</title>
+<script src="/resources/testharness.js?${Math.random()}">${closeScript}
+</head>
+
+<body>
+<div id="log"></div>`;
+ bodies.forEach((body) => {
+ src += '<script>(' + body + ')();' + closeScript;
+ });
+
+ const iframe = document.createElement('iframe');
+
+ document.body.appendChild(iframe);
+ iframe.contentDocument.write(src);
+
+ return new Promise((resolve) => {
+ window.addEventListener('message', function onMessage(e) {
+ if (e.source !== iframe.contentWindow) {
+ return;
+ }
+ if (!e.data || e.data.type !=='complete') {
+ return;
+ }
+ window.removeEventListener('message', onMessage);
+ resolve(e.data);
+ });
+
+ iframe.contentDocument.close();
+ }).then(({ tests, status }) => {
+ const summary = {
+ harness: getEnumProp(status, status.status),
+ tests: {}
+ };
+
+ tests.forEach((test) => {
+ summary.tests[test.name] = getEnumProp(test, test.status);
+ });
+
+ return summary;
+ });
+}
+
+function getEnumProp(object, value) {
+ for (let property in object) {
+ if (!/^[A-Z]+$/.test(property)) {
+ continue;
+ }
+
+ if (object[property] === value) {
+ return property;
+ }
+ }
+}
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_js(TypeError, () => { throw new TypeError(); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'assert_throws_js on a TypeError');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_js(RangeError, () => { throw new RangeError(); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'assert_throws_js on a RangeError');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_js(TypeError, () => { throw new RangeError(); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'assert_throws_js on a TypeError when RangeError is thrown');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_js(Error, () => { throw new TypeError(); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'assert_throws_js on an Error when TypeError is thrown');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_js(Error,
+ () => { throw new DOMException("hello", "Error"); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'assert_throws_js on an Error when a DOMException is thrown');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_js(SyntaxError,
+ () => { throw new DOMException("hey", "SyntaxError"); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'assert_throws_js on a SyntaxError when a DOMException is thrown');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_dom("SyntaxError",
+ () => { throw new DOMException("x", "SyntaxError"); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'assert_throws_dom basic sanity');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_dom(12,
+ () => { throw new DOMException("x", "SyntaxError"); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'assert_throws_dom with numeric code');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_dom("SYNTAX_ERR",
+ () => { throw new DOMException("x", "SyntaxError"); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'assert_throws_dom with string name for code');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_dom("DataError",
+ () => { throw new DOMException("x", "DataError"); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'assert_throws_dom for a code-less DOMException type');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_dom("NoSuchError",
+ () => { throw new DOMException("x", "NoSuchError"); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'assert_throws_dom for a nonexistent DOMException type');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_dom("SyntaxError", () => { throw new SyntaxError(); });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'assert_throws_dom when a non-DOM exception is thrown');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_exactly(5, () => { throw 5; });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'assert_throws_exactly with number');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_exactly("foo", () => { throw "foo"; });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'assert_throws_exactly with string');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_exactly({}, () => { throw {}; });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'assert_throws_exactly with different objects');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ var obj = {};
+ assert_throws_exactly(obj, () => { throw obj; });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'PASS');
+ });
+}, 'assert_throws_exactly with same object');
+
+promise_test(() => {
+ return makeTest(() => {
+ test(() => {
+ assert_throws_exactly(TypeError, () => { throw new TypeError; });
+ });
+ }).then(({harness, tests}) => {
+ assert_equals(harness, 'OK');
+ assert_equals(tests['Document title'], 'FAIL');
+ });
+}, 'assert_throws_exactly with bogus TypeError bits ');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tests/unit/unpaired-surrogates.html b/testing/web-platform/tests/resources/test/tests/unit/unpaired-surrogates.html
new file mode 100644
index 0000000000..b232111326
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tests/unit/unpaired-surrogates.html
@@ -0,0 +1,143 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="/resources/testharness.js"></script>
+ <title>Restrictions on return value from `test`</title>
+</head>
+<body>
+<script>
+function makeTest(...bodies) {
+ const closeScript = '<' + '/script>';
+ let src = `
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Document title</title>
+<script src="/resources/testharness.js?${Math.random()}">${closeScript}
+</head>
+
+<body>
+<div id="log"></div>`;
+ bodies.forEach((body) => {
+ src += '<script>(' + body + ')();' + closeScript;
+ });
+
+ const iframe = document.createElement('iframe');
+
+ document.body.appendChild(iframe);
+ iframe.contentDocument.write(src);
+
+ return new Promise((resolve) => {
+ window.addEventListener('message', function onMessage(e) {
+ if (e.source !== iframe.contentWindow) {
+ return;
+ }
+ if (!e.data || e.data.type !=='complete') {
+ return;
+ }
+ window.removeEventListener('message', onMessage);
+ resolve(e.data);
+ });
+
+ iframe.contentDocument.close();
+ }).then(({ tests, status }) => {
+ const summary = {
+ harness: {
+ status: getEnumProp(status, status.status),
+ message: status.message
+ },
+ tests: {}
+ };
+
+ tests.forEach((test) => {
+ summary.tests[test.name] = getEnumProp(test, test.status);
+ });
+
+ return summary;
+ });
+}
+
+function getEnumProp(object, value) {
+ for (let property in object) {
+ if (!/^[A-Z]+$/.test(property)) {
+ continue;
+ }
+
+ if (object[property] === value) {
+ return property;
+ }
+ }
+}
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ test(() => {}, 'before');
+ test(() => {}, 'U+d7ff is not modified: \ud7ff');
+ test(() => {}, 'U+e000 is not modified: \ue000');
+ test(() => {}, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'OK');
+ assert_equals(harness.message, null);
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests['U+d7ff is not modified: \ud7ff'], 'PASS');
+ assert_equals(tests['U+e000 is not modified: \ue000'], 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'sub-test names which include valid code units');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ test(() => {}, 'before');
+ test(() => {}, 'U+d800U+dfff is not modified: \ud800\udfff');
+ test(() => {}, 'U+dbffU+dc00 is not modified: \udbff\udc00');
+ test(() => {}, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'OK');
+ assert_equals(harness.message, null);
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests['U+d800U+dfff is not modified: \ud800\udfff'], 'PASS');
+ assert_equals(tests['U+dbffU+dc00 is not modified: \udbff\udc00'], 'PASS');
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'sub-test names which include paired surrogates');
+
+promise_test(() => {
+ return makeTest(
+ () => {
+ test(() => {}, 'before');
+ test(() => {}, 'U+d800 must be sanitized: \ud800');
+ test(() => {}, 'U+d800U+d801 must be sanitized: \ud800\ud801');
+ test(() => {}, 'U+dfff must be sanitized: \udfff');
+ test(() => {}, 'U+dc00U+d800U+dc00U+d800 must be sanitized: \udc00\ud800\udc00\ud800');
+ test(() => {}, 'U+dbffU+dfffU+dfff must be sanitized: \udbff\udfff\udfff');
+ test(() => {}, 'after');
+ }
+ ).then(({harness, tests}) => {
+ assert_equals(harness.status, 'OK');
+ assert_equals(harness.message, null);
+ assert_equals(tests.before, 'PASS');
+ assert_equals(tests['U+d800 must be sanitized: U+d800'], 'PASS');
+ assert_equals(tests['U+dfff must be sanitized: U+dfff'], 'PASS');
+ assert_equals(
+ tests['U+d800U+d801 must be sanitized: U+d800U+d801'],
+ 'PASS'
+ );
+ assert_equals(
+ tests['U+dc00U+d800U+dc00U+d800 must be sanitized: U+dc00\ud800\udc00U+d800'],
+ 'PASS'
+ );
+ assert_equals(
+ tests['U+dbffU+dfffU+dfff must be sanitized: \udbff\udfffU+dfff'],
+ 'PASS'
+ );
+ assert_equals(tests.after, 'PASS');
+ });
+}, 'sub-test names which include unpaired surrogates');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/resources/test/tox.ini b/testing/web-platform/tests/resources/test/tox.ini
new file mode 100644
index 0000000000..4fbeb67fb5
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/tox.ini
@@ -0,0 +1,13 @@
+[tox]
+envlist = py37,py38,py39,py310
+skipsdist=True
+
+[testenv]
+passenv=DISPLAY # Necessary for the spawned GeckoDriver process to connect to
+ # the appropriate display.
+
+deps =
+ -r{toxinidir}/../../tools/requirements_pytest.txt
+ -r{toxinidir}/requirements.txt
+
+commands = pytest -vv {posargs}
diff --git a/testing/web-platform/tests/resources/test/wptserver.py b/testing/web-platform/tests/resources/test/wptserver.py
new file mode 100644
index 0000000000..1f913dd96d
--- /dev/null
+++ b/testing/web-platform/tests/resources/test/wptserver.py
@@ -0,0 +1,58 @@
+import logging
+import os
+import subprocess
+import time
+import sys
+import urllib
+
+
+class WPTServer(object):
+ def __init__(self, wpt_root):
+ self.logger = logging.getLogger()
+ self.wpt_root = wpt_root
+
+ # This is a terrible hack to get the default config of wptserve.
+ sys.path.insert(0, os.path.join(wpt_root, "tools"))
+ from serve.serve import build_config
+ with build_config(self.logger) as config:
+ self.host = config["browser_host"]
+ self.http_port = config["ports"]["http"][0]
+ self.https_port = config["ports"]["https"][0]
+
+ self.base_url = 'http://%s:%s' % (self.host, self.http_port)
+ self.https_base_url = 'https://%s:%s' % (self.host, self.https_port)
+
+ def start(self, ssl_context):
+ self.devnull = open(os.devnull, 'w')
+ wptserve_cmd = [os.path.join(self.wpt_root, 'wpt'), 'serve']
+ if sys.executable:
+ wptserve_cmd[0:0] = [sys.executable]
+ self.logger.info('Executing %s' % ' '.join(wptserve_cmd))
+ self.proc = subprocess.Popen(
+ wptserve_cmd,
+ stderr=self.devnull,
+ cwd=self.wpt_root)
+
+ for retry in range(5):
+ # Exponential backoff.
+ time.sleep(2 ** retry)
+ exit_code = self.proc.poll()
+ if exit_code != None:
+ logging.warning('Command "%s" exited with %s', ' '.join(wptserve_cmd), exit_code)
+ break
+ try:
+ urllib.request.urlopen(self.base_url, timeout=1)
+ urllib.request.urlopen(self.https_base_url, timeout=1, context=ssl_context)
+ return
+ except urllib.error.URLError:
+ pass
+
+ raise Exception('Could not start wptserve on %s' % self.base_url)
+
+ def stop(self):
+ self.proc.terminate()
+ self.proc.wait()
+ self.devnull.close()
+
+ def url(self, abs_path):
+ return self.https_base_url + '/' + os.path.relpath(abs_path, self.wpt_root)
diff --git a/testing/web-platform/tests/resources/testdriver-actions.js b/testing/web-platform/tests/resources/testdriver-actions.js
new file mode 100644
index 0000000000..3e5ba74b4c
--- /dev/null
+++ b/testing/web-platform/tests/resources/testdriver-actions.js
@@ -0,0 +1,599 @@
+(function() {
+ let sourceNameIdx = 0;
+
+ /**
+ * @class
+ * Builder for creating a sequence of actions
+ *
+ *
+ * The actions are dispatched once
+ * :js:func:`test_driver.Actions.send` is called. This returns a
+ * promise which resolves once the actions are complete.
+ *
+ * The other methods on :js:class:`test_driver.Actions` object are
+ * used to build the sequence of actions that will be sent. These
+ * return the `Actions` object itself, so the actions sequence can
+ * be constructed by chaining method calls.
+ *
+ * Internally :js:func:`test_driver.Actions.send` invokes
+ * :js:func:`test_driver.action_sequence`.
+ *
+ * @example
+ * let text_box = document.getElementById("text");
+ *
+ * let actions = new test_driver.Actions()
+ * .pointerMove(0, 0, {origin: text_box})
+ * .pointerDown()
+ * .pointerUp()
+ * .addTick()
+ * .keyDown("p")
+ * .keyUp("p");
+ *
+ * await actions.send();
+ *
+ * @param {number} [defaultTickDuration] - The default duration of a
+ * tick. Be default this is set ot 16ms, which is one frame time
+ * based on 60Hz display.
+ */
+ function Actions(defaultTickDuration=16) {
+ this.sourceTypes = new Map([["key", KeySource],
+ ["pointer", PointerSource],
+ ["wheel", WheelSource],
+ ["none", GeneralSource]]);
+ this.sources = new Map();
+ this.sourceOrder = [];
+ for (let sourceType of this.sourceTypes.keys()) {
+ this.sources.set(sourceType, new Map());
+ }
+ this.currentSources = new Map();
+ for (let sourceType of this.sourceTypes.keys()) {
+ this.currentSources.set(sourceType, null);
+ }
+ this.createSource("none");
+ this.tickIdx = 0;
+ this.defaultTickDuration = defaultTickDuration;
+ this.context = null;
+ }
+
+ Actions.prototype = {
+ ButtonType: {
+ LEFT: 0,
+ MIDDLE: 1,
+ RIGHT: 2,
+ BACK: 3,
+ FORWARD: 4,
+ },
+
+ /**
+ * Generate the action sequence suitable for passing to
+ * test_driver.action_sequence
+ *
+ * @returns {Array} Array of WebDriver-compatible actions sequences
+ */
+ serialize: function() {
+ let actions = [];
+ for (let [sourceType, sourceName] of this.sourceOrder) {
+ let source = this.sources.get(sourceType).get(sourceName);
+ let serialized = source.serialize(this.tickIdx + 1, this.defaultTickDuration);
+ if (serialized) {
+ serialized.id = sourceName;
+ actions.push(serialized);
+ }
+ }
+ return actions;
+ },
+
+ /**
+ * Generate and send the action sequence
+ *
+ * @returns {Promise} fulfilled after the sequence is executed,
+ * rejected if any actions fail.
+ */
+ send: function() {
+ let actions;
+ try {
+ actions = this.serialize();
+ } catch(e) {
+ return Promise.reject(e);
+ }
+ return test_driver.action_sequence(actions, this.context);
+ },
+
+ /**
+ * Set the context for the actions
+ *
+ * @param {WindowProxy} context - Context in which to run the action sequence
+ */
+ setContext: function(context) {
+ this.context = context;
+ return this;
+ },
+
+ /**
+ * Get the action source with a particular source type and name.
+ * If no name is passed, a new source with the given type is
+ * created.
+ *
+ * @param {String} type - Source type ('none', 'key', 'pointer', or 'wheel')
+ * @param {String?} name - Name of the source
+ * @returns {Source} Source object for that source.
+ */
+ getSource: function(type, name) {
+ if (!this.sources.has(type)) {
+ throw new Error(`${type} is not a valid action type`);
+ }
+ if (name === null || name === undefined) {
+ name = this.currentSources.get(type);
+ }
+ if (name === null || name === undefined) {
+ return this.createSource(type, null);
+ }
+ return this.sources.get(type).get(name);
+ },
+
+ setSource: function(type, name) {
+ if (!this.sources.has(type)) {
+ throw new Error(`${type} is not a valid action type`);
+ }
+ if (!this.sources.get(type).has(name)) {
+ throw new Error(`${name} is not a valid source for ${type}`);
+ }
+ this.currentSources.set(type, name);
+ return this;
+ },
+
+ /**
+ * Add a new key input source with the given name
+ *
+ * @param {String} name - Name of the key source
+ * @param {Bool} set - Set source as the default key source
+ * @returns {Actions}
+ */
+ addKeyboard: function(name, set=true) {
+ this.createSource("key", name);
+ if (set) {
+ this.setKeyboard(name);
+ }
+ return this;
+ },
+
+ /**
+ * Set the current default key source
+ *
+ * @param {String} name - Name of the key source
+ * @returns {Actions}
+ */
+ setKeyboard: function(name) {
+ this.setSource("key", name);
+ return this;
+ },
+
+ /**
+ * Add a new pointer input source with the given name
+ *
+ * @param {String} type - Name of the pointer source
+ * @param {String} pointerType - Type of pointing device
+ * @param {Bool} set - Set source as the default pointer source
+ * @returns {Actions}
+ */
+ addPointer: function(name, pointerType="mouse", set=true) {
+ this.createSource("pointer", name, {pointerType: pointerType});
+ if (set) {
+ this.setPointer(name);
+ }
+ return this;
+ },
+
+ /**
+ * Set the current default pointer source
+ *
+ * @param {String} name - Name of the pointer source
+ * @returns {Actions}
+ */
+ setPointer: function(name) {
+ this.setSource("pointer", name);
+ return this;
+ },
+
+ /**
+ * Add a new wheel input source with the given name
+ *
+ * @param {String} type - Name of the wheel source
+ * @param {Bool} set - Set source as the default wheel source
+ * @returns {Actions}
+ */
+ addWheel: function(name, set=true) {
+ this.createSource("wheel", name);
+ if (set) {
+ this.setWheel(name);
+ }
+ return this;
+ },
+
+ /**
+ * Set the current default wheel source
+ *
+ * @param {String} name - Name of the wheel source
+ * @returns {Actions}
+ */
+ setWheel: function(name) {
+ this.setSource("wheel", name);
+ return this;
+ },
+
+ createSource: function(type, name, parameters={}) {
+ if (!this.sources.has(type)) {
+ throw new Error(`${type} is not a valid action type`);
+ }
+ let sourceNames = new Set();
+ for (let [_, name] of this.sourceOrder) {
+ sourceNames.add(name);
+ }
+ if (!name) {
+ do {
+ name = "" + sourceNameIdx++;
+ } while (sourceNames.has(name))
+ } else {
+ if (sourceNames.has(name)) {
+ throw new Error(`Alreay have a source of type ${type} named ${name}.`);
+ }
+ }
+ this.sources.get(type).set(name, new (this.sourceTypes.get(type))(parameters));
+ this.currentSources.set(type, name);
+ this.sourceOrder.push([type, name]);
+ return this.sources.get(type).get(name);
+ },
+
+ /**
+ * Insert a new actions tick
+ *
+ * @param {Number?} duration - Minimum length of the tick in ms.
+ * @returns {Actions}
+ */
+ addTick: function(duration) {
+ this.tickIdx += 1;
+ if (duration) {
+ this.pause(duration);
+ }
+ return this;
+ },
+
+ /**
+ * Add a pause to the current tick
+ *
+ * @param {Number?} duration - Minimum length of the tick in ms.
+ * @param {String} sourceType - source type
+ * @param {String?} sourceName - Named key, pointer or wheel source to use
+ * or null for the default key, pointer or
+ * wheel source
+ * @returns {Actions}
+ */
+ pause: function(duration=0, sourceType="none", {sourceName=null}={}) {
+ if (sourceType=="none")
+ this.getSource("none").addPause(this, duration);
+ else
+ this.getSource(sourceType, sourceName).addPause(this, duration);
+ return this;
+ },
+
+ /**
+ * Create a keyDown event for the current default key source
+ *
+ * @param {String} key - Key to press
+ * @param {String?} sourceName - Named key source to use or null for the default key source
+ * @returns {Actions}
+ */
+ keyDown: function(key, {sourceName=null}={}) {
+ let source = this.getSource("key", sourceName);
+ source.keyDown(this, key);
+ return this;
+ },
+
+ /**
+ * Create a keyDown event for the current default key source
+ *
+ * @param {String} key - Key to release
+ * @param {String?} sourceName - Named key source to use or null for the default key source
+ * @returns {Actions}
+ */
+ keyUp: function(key, {sourceName=null}={}) {
+ let source = this.getSource("key", sourceName);
+ source.keyUp(this, key);
+ return this;
+ },
+
+ /**
+ * Create a pointerDown event for the current default pointer source
+ *
+ * @param {String} button - Button to press
+ * @param {String?} sourceName - Named pointer source to use or null for the default
+ * pointer source
+ * @returns {Actions}
+ */
+ pointerDown: function({button=this.ButtonType.LEFT, sourceName=null,
+ width, height, pressure, tangentialPressure,
+ tiltX, tiltY, twist, altitudeAngle, azimuthAngle}={}) {
+ let source = this.getSource("pointer", sourceName);
+ source.pointerDown(this, button, width, height, pressure, tangentialPressure,
+ tiltX, tiltY, twist, altitudeAngle, azimuthAngle);
+ return this;
+ },
+
+ /**
+ * Create a pointerUp event for the current default pointer source
+ *
+ * @param {String} button - Button to release
+ * @param {String?} sourceName - Named pointer source to use or null for the default pointer
+ * source
+ * @returns {Actions}
+ */
+ pointerUp: function({button=this.ButtonType.LEFT, sourceName=null}={}) {
+ let source = this.getSource("pointer", sourceName);
+ source.pointerUp(this, button);
+ return this;
+ },
+
+ /**
+ * Create a move event for the current default pointer source
+ *
+ * @param {Number} x - Destination x coordinate
+ * @param {Number} y - Destination y coordinate
+ * @param {String|Element} origin - Origin of the coordinate system.
+ * Either "pointer", "viewport" or an Element
+ * @param {Number?} duration - Time in ms for the move
+ * @param {String?} sourceName - Named pointer source to use or null for the default pointer
+ * source
+ * @returns {Actions}
+ */
+ pointerMove: function(x, y,
+ {origin="viewport", duration, sourceName=null,
+ width, height, pressure, tangentialPressure,
+ tiltX, tiltY, twist, altitudeAngle, azimuthAngle}={}) {
+ let source = this.getSource("pointer", sourceName);
+ source.pointerMove(this, x, y, duration, origin, width, height, pressure,
+ tangentialPressure, tiltX, tiltY, twist, altitudeAngle,
+ azimuthAngle);
+ return this;
+ },
+
+ /**
+ * Create a scroll event for the current default wheel source
+ *
+ * @param {Number} x - mouse cursor x coordinate
+ * @param {Number} y - mouse cursor y coordinate
+ * @param {Number} deltaX - scroll delta value along the x-axis in pixels
+ * @param {Number} deltaY - scroll delta value along the y-axis in pixels
+ * @param {String|Element} origin - Origin of the coordinate system.
+ * Either "viewport" or an Element
+ * @param {Number?} duration - Time in ms for the scroll
+ * @param {String?} sourceName - Named wheel source to use or null for the
+ * default wheel source
+ * @returns {Actions}
+ */
+ scroll: function(x, y, deltaX, deltaY,
+ {origin="viewport", duration, sourceName=null}={}) {
+ let source = this.getSource("wheel", sourceName);
+ source.scroll(this, x, y, deltaX, deltaY, duration, origin);
+ return this;
+ },
+ };
+
+ function GeneralSource() {
+ this.actions = new Map();
+ }
+
+ GeneralSource.prototype = {
+ serialize: function(tickCount, defaultTickDuration) {
+ let actions = [];
+ let data = {"type": "none", "actions": actions};
+ for (let i=0; i<tickCount; i++) {
+ if (this.actions.has(i)) {
+ actions.push(this.actions.get(i));
+ } else {
+ actions.push({"type": "pause", duration: defaultTickDuration});
+ }
+ }
+ return data;
+ },
+
+ addPause: function(actions, duration) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ throw new Error(`Already have a pause action for the current tick`);
+ }
+ this.actions.set(tick, {type: "pause", duration: duration});
+ },
+ };
+
+ function KeySource() {
+ this.actions = new Map();
+ }
+
+ KeySource.prototype = {
+ serialize: function(tickCount) {
+ if (!this.actions.size) {
+ return undefined;
+ }
+ let actions = [];
+ let data = {"type": "key", "actions": actions};
+ for (let i=0; i<tickCount; i++) {
+ if (this.actions.has(i)) {
+ actions.push(this.actions.get(i));
+ } else {
+ actions.push({"type": "pause"});
+ }
+ }
+ return data;
+ },
+
+ keyDown: function(actions, key) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ tick = actions.addTick().tickIdx;
+ }
+ this.actions.set(tick, {type: "keyDown", value: key});
+ },
+
+ keyUp: function(actions, key) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ tick = actions.addTick().tickIdx;
+ }
+ this.actions.set(tick, {type: "keyUp", value: key});
+ },
+
+ addPause: function(actions, duration) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ tick = actions.addTick().tickIdx;
+ }
+ this.actions.set(tick, {type: "pause", duration: duration});
+ },
+ };
+
+ function PointerSource(parameters={pointerType: "mouse"}) {
+ let pointerType = parameters.pointerType || "mouse";
+ if (!["mouse", "pen", "touch"].includes(pointerType)) {
+ throw new Error(`Invalid pointerType ${pointerType}`);
+ }
+ this.type = pointerType;
+ this.actions = new Map();
+ }
+
+ function setPointerProperties(action, width, height, pressure, tangentialPressure,
+ tiltX, tiltY, twist, altitudeAngle, azimuthAngle) {
+ if (width) {
+ action.width = width;
+ }
+ if (height) {
+ action.height = height;
+ }
+ if (pressure) {
+ action.pressure = pressure;
+ }
+ if (tangentialPressure) {
+ action.tangentialPressure = tangentialPressure;
+ }
+ if (tiltX) {
+ action.tiltX = tiltX;
+ }
+ if (tiltY) {
+ action.tiltY = tiltY;
+ }
+ if (twist) {
+ action.twist = twist;
+ }
+ if (altitudeAngle) {
+ action.altitudeAngle = altitudeAngle;
+ }
+ if (azimuthAngle) {
+ action.azimuthAngle = azimuthAngle;
+ }
+ return action;
+ }
+
+ PointerSource.prototype = {
+ serialize: function(tickCount) {
+ if (!this.actions.size) {
+ return undefined;
+ }
+ let actions = [];
+ let data = {"type": "pointer", "actions": actions, "parameters": {"pointerType": this.type}};
+ for (let i=0; i<tickCount; i++) {
+ if (this.actions.has(i)) {
+ actions.push(this.actions.get(i));
+ } else {
+ actions.push({"type": "pause"});
+ }
+ }
+ return data;
+ },
+
+ pointerDown: function(actions, button, width, height, pressure, tangentialPressure,
+ tiltX, tiltY, twist, altitudeAngle, azimuthAngle) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ tick = actions.addTick().tickIdx;
+ }
+ let actionProperties = setPointerProperties({type: "pointerDown", button}, width, height,
+ pressure, tangentialPressure, tiltX, tiltY,
+ twist, altitudeAngle, azimuthAngle);
+ this.actions.set(tick, actionProperties);
+ },
+
+ pointerUp: function(actions, button) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ tick = actions.addTick().tickIdx;
+ }
+ this.actions.set(tick, {type: "pointerUp", button});
+ },
+
+ pointerMove: function(actions, x, y, duration, origin, width, height, pressure,
+ tangentialPressure, tiltX, tiltY, twist, altitudeAngle, azimuthAngle) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ tick = actions.addTick().tickIdx;
+ }
+ let moveAction = {type: "pointerMove", x, y, origin};
+ if (duration) {
+ moveAction.duration = duration;
+ }
+ let actionProperties = setPointerProperties(moveAction, width, height, pressure,
+ tangentialPressure, tiltX, tiltY, twist,
+ altitudeAngle, azimuthAngle);
+ this.actions.set(tick, actionProperties);
+ },
+
+ addPause: function(actions, duration) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ tick = actions.addTick().tickIdx;
+ }
+ this.actions.set(tick, {type: "pause", duration: duration});
+ },
+ };
+
+ function WheelSource() {
+ this.actions = new Map();
+ }
+
+ WheelSource.prototype = {
+ serialize: function(tickCount) {
+ if (!this.actions.size) {
+ return undefined;
+ }
+ let actions = [];
+ let data = {"type": "wheel", "actions": actions};
+ for (let i=0; i<tickCount; i++) {
+ if (this.actions.has(i)) {
+ actions.push(this.actions.get(i));
+ } else {
+ actions.push({"type": "pause"});
+ }
+ }
+ return data;
+ },
+
+ scroll: function(actions, x, y, deltaX, deltaY, duration, origin) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ tick = actions.addTick().tickIdx;
+ }
+ this.actions.set(tick, {type: "scroll", x, y, deltaX, deltaY, origin});
+ if (duration) {
+ this.actions.get(tick).duration = duration;
+ }
+ },
+
+ addPause: function(actions, duration) {
+ let tick = actions.tickIdx;
+ if (this.actions.has(tick)) {
+ tick = actions.addTick().tickIdx;
+ }
+ this.actions.set(tick, {type: "pause", duration: duration});
+ },
+ };
+
+ test_driver.Actions = Actions;
+})();
diff --git a/testing/web-platform/tests/resources/testdriver-vendor.js b/testing/web-platform/tests/resources/testdriver-vendor.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/resources/testdriver-vendor.js
diff --git a/testing/web-platform/tests/resources/testdriver-vendor.js.headers b/testing/web-platform/tests/resources/testdriver-vendor.js.headers
new file mode 100644
index 0000000000..5e8f640c66
--- /dev/null
+++ b/testing/web-platform/tests/resources/testdriver-vendor.js.headers
@@ -0,0 +1,2 @@
+Content-Type: text/javascript; charset=utf-8
+Cache-Control: max-age=3600
diff --git a/testing/web-platform/tests/resources/testdriver.js b/testing/web-platform/tests/resources/testdriver.js
new file mode 100644
index 0000000000..446b033b0a
--- /dev/null
+++ b/testing/web-platform/tests/resources/testdriver.js
@@ -0,0 +1,809 @@
+(function() {
+ "use strict";
+ var idCounter = 0;
+ let testharness_context = null;
+
+ function getInViewCenterPoint(rect) {
+ var left = Math.max(0, rect.left);
+ var right = Math.min(window.innerWidth, rect.right);
+ var top = Math.max(0, rect.top);
+ var bottom = Math.min(window.innerHeight, rect.bottom);
+
+ var x = 0.5 * (left + right);
+ var y = 0.5 * (top + bottom);
+
+ return [x, y];
+ }
+
+ function getPointerInteractablePaintTree(element) {
+ let elementDocument = element.ownerDocument;
+ if (!elementDocument.contains(element)) {
+ return [];
+ }
+
+ var rectangles = element.getClientRects();
+
+ if (rectangles.length === 0) {
+ return [];
+ }
+
+ var centerPoint = getInViewCenterPoint(rectangles[0]);
+
+ if ("elementsFromPoint" in elementDocument) {
+ return elementDocument.elementsFromPoint(centerPoint[0], centerPoint[1]);
+ } else if ("msElementsFromPoint" in elementDocument) {
+ var rv = elementDocument.msElementsFromPoint(centerPoint[0], centerPoint[1]);
+ return Array.prototype.slice.call(rv ? rv : []);
+ } else {
+ throw new Error("document.elementsFromPoint unsupported");
+ }
+ }
+
+ function inView(element) {
+ var pointerInteractablePaintTree = getPointerInteractablePaintTree(element);
+ return pointerInteractablePaintTree.indexOf(element) !== -1;
+ }
+
+
+ /**
+ * @namespace {test_driver}
+ */
+ window.test_driver = {
+ /**
+ * Set the context in which testharness.js is loaded
+ *
+ * @param {WindowProxy} context - the window containing testharness.js
+ **/
+ set_test_context: function(context) {
+ if (window.test_driver_internal.set_test_context) {
+ window.test_driver_internal.set_test_context(context);
+ }
+ testharness_context = context;
+ },
+
+ /**
+ * postMessage to the context containing testharness.js
+ *
+ * @param {Object} msg - the data to POST
+ **/
+ message_test: function(msg) {
+ let target = testharness_context;
+ if (testharness_context === null) {
+ target = window;
+ }
+ target.postMessage(msg, "*");
+ },
+
+ /**
+ * Trigger user interaction in order to grant additional privileges to
+ * a provided function.
+ *
+ * See `Tracking user activation
+ * <https://html.spec.whatwg.org/multipage/interaction.html#tracking-user-activation>`_.
+ *
+ * @example
+ * var mediaElement = document.createElement('video');
+ *
+ * test_driver.bless('initiate media playback', function () {
+ * mediaElement.play();
+ * });
+ *
+ * @param {String} intent - a description of the action which must be
+ * triggered by user interaction
+ * @param {Function} action - code requiring escalated privileges
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled following user interaction and
+ * execution of the provided `action` function;
+ * rejected if interaction fails or the provided
+ * function throws an error
+ */
+ bless: function(intent, action, context=null) {
+ let contextDocument = context ? context.document : document;
+ var button = contextDocument.createElement("button");
+ button.innerHTML = "This test requires user interaction.<br />" +
+ "Please click here to allow " + intent + ".";
+ button.id = "wpt-test-driver-bless-" + (idCounter += 1);
+ const elem = contextDocument.body || contextDocument.documentElement;
+ elem.appendChild(button);
+
+ let wait_click = new Promise(resolve => button.addEventListener("click", resolve));
+
+ return test_driver.click(button)
+ .then(wait_click)
+ .then(function() {
+ button.remove();
+
+ if (typeof action === "function") {
+ return action();
+ }
+ return null;
+ });
+ },
+
+ /**
+ * Triggers a user-initiated click
+ *
+ * If ``element`` isn't inside the
+ * viewport, it will be scrolled into view before the click
+ * occurs.
+ *
+ * If ``element`` is from a different browsing context, the
+ * command will be run in that context.
+ *
+ * Matches the behaviour of the `Element Click
+ * <https://w3c.github.io/webdriver/#element-click>`_
+ * WebDriver command.
+ *
+ * **Note:** If the element to be clicked does not have a
+ * unique ID, the document must not have any DOM mutations
+ * made between the function being called and the promise
+ * settling.
+ *
+ * @param {Element} element - element to be clicked
+ * @returns {Promise} fulfilled after click occurs, or rejected in
+ * the cases the WebDriver command errors
+ */
+ click: function(element) {
+ if (!inView(element)) {
+ element.scrollIntoView({behavior: "instant",
+ block: "end",
+ inline: "nearest"});
+ }
+
+ var pointerInteractablePaintTree = getPointerInteractablePaintTree(element);
+ if (pointerInteractablePaintTree.length === 0 ||
+ !element.contains(pointerInteractablePaintTree[0])) {
+ return Promise.reject(new Error("element click intercepted error"));
+ }
+
+ var rect = element.getClientRects()[0];
+ var centerPoint = getInViewCenterPoint(rect);
+ return window.test_driver_internal.click(element,
+ {x: centerPoint[0],
+ y: centerPoint[1]});
+ },
+
+ /**
+ * Deletes all cookies.
+ *
+ * Matches the behaviour of the `Delete All Cookies
+ * <https://w3c.github.io/webdriver/#delete-all-cookies>`_
+ * WebDriver command.
+ *
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after cookies are deleted, or rejected in
+ * the cases the WebDriver command errors
+ */
+ delete_all_cookies: function(context=null) {
+ return window.test_driver_internal.delete_all_cookies(context);
+ },
+
+ /**
+ * Get details for all cookies in the current context.
+ * See https://w3c.github.io/webdriver/#get-all-cookies
+ *
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} Returns an array of cookies objects as defined in the spec:
+ * https://w3c.github.io/webdriver/#cookies
+ */
+ get_all_cookies: function(context=null) {
+ return window.test_driver_internal.get_all_cookies(context);
+ },
+
+ /**
+ * Get details for a cookie in the current context by name if it exists.
+ * See https://w3c.github.io/webdriver/#get-named-cookie
+ *
+ * @param {String} name - The name of the cookie to get.
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} Returns the matching cookie as defined in the spec:
+ * https://w3c.github.io/webdriver/#cookies
+ * Rejected if no such cookie exists.
+ */
+ get_named_cookie: async function(name, context=null) {
+ let cookie = await window.test_driver_internal.get_named_cookie(name, context);
+ if (!cookie) {
+ throw new Error("no such cookie");
+ }
+ return cookie;
+ },
+
+ /**
+ * Get Computed Label for an element.
+ *
+ * This matches the behaviour of the
+ * `Get Computed Label
+ * <https://w3c.github.io/webdriver/#dfn-get-computed-label>`_
+ * WebDriver command.
+ *
+ * @param {Element} element
+ * @returns {Promise} fulfilled after the computed label is returned, or
+ * rejected in the cases the WebDriver command errors
+ */
+ get_computed_label: async function(element) {
+ let label = await window.test_driver_internal.get_computed_label(element);
+ return label;
+ },
+
+ /**
+ * Get Computed Role for an element.
+ *
+ * This matches the behaviour of the
+ * `Get Computed Label
+ * <https://w3c.github.io/webdriver/#dfn-get-computed-role>`_
+ * WebDriver command.
+ *
+ * @param {Element} element
+ * @returns {Promise} fulfilled after the computed role is returned, or
+ * rejected in the cases the WebDriver command errors
+ */
+ get_computed_role: async function(element) {
+ let role = await window.test_driver_internal.get_computed_role(element);
+ return role;
+ },
+
+ /**
+ * Send keys to an element.
+ *
+ * If ``element`` isn't inside the
+ * viewport, it will be scrolled into view before the click
+ * occurs.
+ *
+ * If ``element`` is from a different browsing context, the
+ * command will be run in that context.
+ *
+ * To send special keys, send the respective key's codepoint,
+ * as defined by `WebDriver
+ * <https://w3c.github.io/webdriver/#keyboard-actions>`_. For
+ * example, the "tab" key is represented as "``\uE004``".
+ *
+ * **Note:** these special-key codepoints are not necessarily
+ * what you would expect. For example, <kbd>Esc</kbd> is the
+ * invalid Unicode character ``\uE00C``, not the ``\u001B`` Escape
+ * character from ASCII.
+ *
+ * This matches the behaviour of the
+ * `Send Keys
+ * <https://w3c.github.io/webdriver/#element-send-keys>`_
+ * WebDriver command.
+ *
+ * **Note:** If the element to be clicked does not have a
+ * unique ID, the document must not have any DOM mutations
+ * made between the function being called and the promise
+ * settling.
+ *
+ * @param {Element} element - element to send keys to
+ * @param {String} keys - keys to send to the element
+ * @returns {Promise} fulfilled after keys are sent, or rejected in
+ * the cases the WebDriver command errors
+ */
+ send_keys: function(element, keys) {
+ if (!inView(element)) {
+ element.scrollIntoView({behavior: "instant",
+ block: "end",
+ inline: "nearest"});
+ }
+
+ var pointerInteractablePaintTree = getPointerInteractablePaintTree(element);
+ if (pointerInteractablePaintTree.length === 0 ||
+ !element.contains(pointerInteractablePaintTree[0])) {
+ return Promise.reject(new Error("element send_keys intercepted error"));
+ }
+
+ return window.test_driver_internal.send_keys(element, keys);
+ },
+
+ /**
+ * Freeze the current page
+ *
+ * The freeze function transitions the page from the HIDDEN state to
+ * the FROZEN state as described in `Lifecycle API for Web Pages
+ * <https://github.com/WICG/page-lifecycle/blob/master/README.md>`_.
+ *
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the freeze request is sent, or rejected
+ * in case the WebDriver command errors
+ */
+ freeze: function(context=null) {
+ return window.test_driver_internal.freeze();
+ },
+
+ /**
+ * Minimizes the browser window.
+ *
+ * Matches the the behaviour of the `Minimize
+ * <https://www.w3.org/TR/webdriver/#minimize-window>`_
+ * WebDriver command
+ *
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled with the previous {@link
+ * https://www.w3.org/TR/webdriver/#dfn-windowrect-object|WindowRect}
+ * value, after the window is minimized.
+ */
+ minimize_window: function(context=null) {
+ return window.test_driver_internal.minimize_window(context);
+ },
+
+ /**
+ * Restore the window from minimized/maximized state to a given rect.
+ *
+ * Matches the behaviour of the `Set Window Rect
+ * <https://www.w3.org/TR/webdriver/#set-window-rect>`_
+ * WebDriver command
+ *
+ * @param {Object} rect - A {@link
+ * https://www.w3.org/TR/webdriver/#dfn-windowrect-object|WindowRect}
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the window is restored to the given rect.
+ */
+ set_window_rect: function(rect, context=null) {
+ return window.test_driver_internal.set_window_rect(rect, context);
+ },
+
+ /**
+ * Send a sequence of actions
+ *
+ * This function sends a sequence of actions to perform.
+ *
+ * Matches the behaviour of the `Actions
+ * <https://w3c.github.io/webdriver/#actions>`_ feature in
+ * WebDriver.
+ *
+ * Authors are encouraged to use the
+ * :js:class:`test_driver.Actions` builder rather than
+ * invoking this API directly.
+ *
+ * @param {Array} actions - an array of actions. The format is
+ * the same as the actions property
+ * of the `Perform Actions
+ * <https://w3c.github.io/webdriver/#perform-actions>`_
+ * WebDriver command. Each element is
+ * an object representing an input
+ * source and each input source
+ * itself has an actions property
+ * detailing the behaviour of that
+ * source at each timestep (or
+ * tick). Authors are not expected to
+ * construct the actions sequence by
+ * hand, but to use the builder api
+ * provided in testdriver-actions.js
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the actions are performed, or rejected in
+ * the cases the WebDriver command errors
+ */
+ action_sequence: function(actions, context=null) {
+ return window.test_driver_internal.action_sequence(actions, context);
+ },
+
+ /**
+ * Generates a test report on the current page
+ *
+ * The generate_test_report function generates a report (to be
+ * observed by ReportingObserver) for testing purposes.
+ *
+ * Matches the `Generate Test Report
+ * <https://w3c.github.io/reporting/#generate-test-report-command>`_
+ * WebDriver command.
+ *
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the report is generated, or
+ * rejected if the report generation fails
+ */
+ generate_test_report: function(message, context=null) {
+ return window.test_driver_internal.generate_test_report(message, context);
+ },
+
+ /**
+ * Sets the state of a permission
+ *
+ * This function simulates a user setting a permission into a
+ * particular state.
+ *
+ * Matches the `Set Permission
+ * <https://w3c.github.io/permissions/#set-permission-command>`_
+ * WebDriver command.
+ *
+ * @example
+ * await test_driver.set_permission({ name: "background-fetch" }, "denied");
+ * await test_driver.set_permission({ name: "push", userVisibleOnly: true }, "granted");
+ *
+ * @param {PermissionDescriptor} descriptor - a `PermissionDescriptor
+ * <https://w3c.github.io/permissions/#dom-permissiondescriptor>`_
+ * dictionary.
+ * @param {String} state - the state of the permission
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ * @returns {Promise} fulfilled after the permission is set, or rejected if setting the
+ * permission fails
+ */
+ set_permission: function(descriptor, state, context=null) {
+ let permission_params = {
+ descriptor,
+ state,
+ };
+ return window.test_driver_internal.set_permission(permission_params, context);
+ },
+
+ /**
+ * Creates a virtual authenticator
+ *
+ * This function creates a virtual authenticator for use with
+ * the U2F and WebAuthn APIs.
+ *
+ * Matches the `Add Virtual Authenticator
+ * <https://w3c.github.io/webauthn/#sctn-automation-add-virtual-authenticator>`_
+ * WebDriver command.
+ *
+ * @param {Object} config - an `Authenticator Configuration
+ * <https://w3c.github.io/webauthn/#authenticator-configuration>`_
+ * object
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the authenticator is added, or
+ * rejected in the cases the WebDriver command
+ * errors. Returns the ID of the authenticator
+ */
+ add_virtual_authenticator: function(config, context=null) {
+ return window.test_driver_internal.add_virtual_authenticator(config, context);
+ },
+
+ /**
+ * Removes a virtual authenticator
+ *
+ * This function removes a virtual authenticator that has been
+ * created by :js:func:`add_virtual_authenticator`.
+ *
+ * Matches the `Remove Virtual Authenticator
+ * <https://w3c.github.io/webauthn/#sctn-automation-remove-virtual-authenticator>`_
+ * WebDriver command.
+ *
+ * @param {String} authenticator_id - the ID of the authenticator to be
+ * removed.
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the authenticator is removed, or
+ * rejected in the cases the WebDriver command
+ * errors
+ */
+ remove_virtual_authenticator: function(authenticator_id, context=null) {
+ return window.test_driver_internal.remove_virtual_authenticator(authenticator_id, context);
+ },
+
+ /**
+ * Adds a credential to a virtual authenticator
+ *
+ * Matches the `Add Credential
+ * <https://w3c.github.io/webauthn/#sctn-automation-add-credential>`_
+ * WebDriver command.
+ *
+ * @param {String} authenticator_id - the ID of the authenticator
+ * @param {Object} credential - A `Credential Parameters
+ * <https://w3c.github.io/webauthn/#credential-parameters>`_
+ * object
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the credential is added, or
+ * rejected in the cases the WebDriver command
+ * errors
+ */
+ add_credential: function(authenticator_id, credential, context=null) {
+ return window.test_driver_internal.add_credential(authenticator_id, credential, context);
+ },
+
+ /**
+ * Gets all the credentials stored in an authenticator
+ *
+ * This function retrieves all the credentials (added via the U2F API,
+ * WebAuthn, or the add_credential function) stored in a virtual
+ * authenticator
+ *
+ * Matches the `Get Credentials
+ * <https://w3c.github.io/webauthn/#sctn-automation-get-credentials>`_
+ * WebDriver command.
+ *
+ * @param {String} authenticator_id - the ID of the authenticator
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the credentials are
+ * returned, or rejected in the cases the
+ * WebDriver command errors. Returns an
+ * array of `Credential Parameters
+ * <https://w3c.github.io/webauthn/#credential-parameters>`_
+ */
+ get_credentials: function(authenticator_id, context=null) {
+ return window.test_driver_internal.get_credentials(authenticator_id, context=null);
+ },
+
+ /**
+ * Remove a credential stored in an authenticator
+ *
+ * Matches the `Remove Credential
+ * <https://w3c.github.io/webauthn/#sctn-automation-remove-credential>`_
+ * WebDriver command.
+ *
+ * @param {String} authenticator_id - the ID of the authenticator
+ * @param {String} credential_id - the ID of the credential
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the credential is removed, or
+ * rejected in the cases the WebDriver command
+ * errors.
+ */
+ remove_credential: function(authenticator_id, credential_id, context=null) {
+ return window.test_driver_internal.remove_credential(authenticator_id, credential_id, context);
+ },
+
+ /**
+ * Removes all the credentials stored in a virtual authenticator
+ *
+ * Matches the `Remove All Credentials
+ * <https://w3c.github.io/webauthn/#sctn-automation-remove-all-credentials>`_
+ * WebDriver command.
+ *
+ * @param {String} authenticator_id - the ID of the authenticator
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the credentials are removed, or
+ * rejected in the cases the WebDriver command
+ * errors.
+ */
+ remove_all_credentials: function(authenticator_id, context=null) {
+ return window.test_driver_internal.remove_all_credentials(authenticator_id, context);
+ },
+
+ /**
+ * Sets the User Verified flag on an authenticator
+ *
+ * Sets whether requests requiring user verification will succeed or
+ * fail on a given virtual authenticator
+ *
+ * Matches the `Set User Verified
+ * <https://w3c.github.io/webauthn/#sctn-automation-set-user-verified>`_
+ * WebDriver command.
+ *
+ * @param {String} authenticator_id - the ID of the authenticator
+ * @param {boolean} uv - the User Verified flag
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ */
+ set_user_verified: function(authenticator_id, uv, context=null) {
+ return window.test_driver_internal.set_user_verified(authenticator_id, uv, context);
+ },
+
+ /**
+ * Sets the storage access rule for an origin when embedded
+ * in a third-party context.
+ *
+ * Matches the `Set Storage Access
+ * <https://privacycg.github.io/storage-access/#set-storage-access-command>`_
+ * WebDriver command.
+ *
+ * @param {String} origin - A third-party origin to block or allow.
+ * May be "*" to indicate all origins.
+ * @param {String} embedding_origin - an embedding (first-party) origin
+ * on which {origin}'s access should
+ * be blocked or allowed.
+ * May be "*" to indicate all origins.
+ * @param {String} state - The storage access setting.
+ * Must be either "allowed" or "blocked".
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} Fulfilled after the storage access rule has been
+ * set, or rejected if setting the rule fails.
+ */
+ set_storage_access: function(origin, embedding_origin, state, context=null) {
+ if (state !== "allowed" && state !== "blocked") {
+ throw new Error("storage access status must be 'allowed' or 'blocked'");
+ }
+ const blocked = state === "blocked";
+ return window.test_driver_internal.set_storage_access(origin, embedding_origin, blocked, context);
+ },
+
+ /**
+ * Sets the current transaction automation mode for Secure Payment
+ * Confirmation.
+ *
+ * This function places `Secure Payment
+ * Confirmation <https://w3c.github.io/secure-payment-confirmation>`_ into
+ * an automated 'autoaccept' or 'autoreject' mode, to allow testing
+ * without user interaction with the transaction UX prompt.
+ *
+ * Matches the `Set SPC Transaction Mode
+ * <https://w3c.github.io/secure-payment-confirmation/#sctn-automation-set-spc-transaction-mode>`_
+ * WebDriver command.
+ *
+ * @example
+ * await test_driver.set_spc_transaction_mode("autoaccept");
+ * test.add_cleanup(() => {
+ * return test_driver.set_spc_transaction_mode("none");
+ * });
+ *
+ * // Assumption: `request` is a PaymentRequest with a secure-payment-confirmation
+ * // payment method.
+ * const response = await request.show();
+ *
+ * @param {String} mode - The `transaction mode
+ * <https://w3c.github.io/secure-payment-confirmation/#enumdef-transactionautomationmode>`_
+ * to set. Must be one of "``none``",
+ * "``autoaccept``", or
+ * "``autoreject``".
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} Fulfilled after the transaction mode has been set,
+ * or rejected if setting the mode fails.
+ */
+ set_spc_transaction_mode: function(mode, context=null) {
+ return window.test_driver_internal.set_spc_transaction_mode(mode, context);
+ },
+ };
+
+ window.test_driver_internal = {
+ /**
+ * This flag should be set to `true` by any code which implements the
+ * internal methods defined below for automation purposes. Doing so
+ * allows the library to signal failure immediately when an automated
+ * implementation of one of the methods is not available.
+ */
+ in_automation: false,
+
+ async click(element, coords) {
+ if (this.in_automation) {
+ throw new Error("click() is not implemented by testdriver-vendor.js");
+ }
+
+ return new Promise(function(resolve, reject) {
+ element.addEventListener("click", resolve);
+ });
+ },
+
+ async delete_all_cookies(context=null) {
+ throw new Error("delete_all_cookies() is not implemented by testdriver-vendor.js");
+ },
+
+ async get_all_cookies(context=null) {
+ throw new Error("get_all_cookies() is not implemented by testdriver-vendor.js");
+ },
+
+ async get_named_cookie(name, context=null) {
+ throw new Error("get_named_cookie() is not implemented by testdriver-vendor.js");
+ },
+
+ async send_keys(element, keys) {
+ if (this.in_automation) {
+ throw new Error("send_keys() is not implemented by testdriver-vendor.js");
+ }
+
+ return new Promise(function(resolve, reject) {
+ var seen = "";
+
+ function remove() {
+ element.removeEventListener("keydown", onKeyDown);
+ }
+
+ function onKeyDown(event) {
+ if (event.key.length > 1) {
+ return;
+ }
+
+ seen += event.key;
+
+ if (keys.indexOf(seen) !== 0) {
+ reject(new Error("Unexpected key sequence: " + seen));
+ remove();
+ } else if (seen === keys) {
+ resolve();
+ remove();
+ }
+ }
+
+ element.addEventListener("keydown", onKeyDown);
+ });
+ },
+
+ async freeze(context=null) {
+ throw new Error("freeze() is not implemented by testdriver-vendor.js");
+ },
+
+ async minimize_window(context=null) {
+ throw new Error("minimize_window() is not implemented by testdriver-vendor.js");
+ },
+
+ async set_window_rect(rect, context=null) {
+ throw new Error("set_window_rect() is not implemented by testdriver-vendor.js");
+ },
+
+ async action_sequence(actions, context=null) {
+ throw new Error("action_sequence() is not implemented by testdriver-vendor.js");
+ },
+
+ async generate_test_report(message, context=null) {
+ throw new Error("generate_test_report() is not implemented by testdriver-vendor.js");
+ },
+
+ async set_permission(permission_params, context=null) {
+ throw new Error("set_permission() is not implemented by testdriver-vendor.js");
+ },
+
+ async add_virtual_authenticator(config, context=null) {
+ throw new Error("add_virtual_authenticator() is not implemented by testdriver-vendor.js");
+ },
+
+ async remove_virtual_authenticator(authenticator_id, context=null) {
+ throw new Error("remove_virtual_authenticator() is not implemented by testdriver-vendor.js");
+ },
+
+ async add_credential(authenticator_id, credential, context=null) {
+ throw new Error("add_credential() is not implemented by testdriver-vendor.js");
+ },
+
+ async get_credentials(authenticator_id, context=null) {
+ throw new Error("get_credentials() is not implemented by testdriver-vendor.js");
+ },
+
+ async remove_credential(authenticator_id, credential_id, context=null) {
+ throw new Error("remove_credential() is not implemented by testdriver-vendor.js");
+ },
+
+ async remove_all_credentials(authenticator_id, context=null) {
+ throw new Error("remove_all_credentials() is not implemented by testdriver-vendor.js");
+ },
+
+ async set_user_verified(authenticator_id, uv, context=null) {
+ throw new Error("set_user_verified() is not implemented by testdriver-vendor.js");
+ },
+
+ async set_storage_access(origin, embedding_origin, blocked, context=null) {
+ throw new Error("set_storage_access() is not implemented by testdriver-vendor.js");
+ },
+
+ async set_spc_transaction_mode(mode, context=null) {
+ throw new Error("set_spc_transaction_mode() is not implemented by testdriver-vendor.js");
+ },
+
+ };
+})();
diff --git a/testing/web-platform/tests/resources/testdriver.js.headers b/testing/web-platform/tests/resources/testdriver.js.headers
new file mode 100644
index 0000000000..5e8f640c66
--- /dev/null
+++ b/testing/web-platform/tests/resources/testdriver.js.headers
@@ -0,0 +1,2 @@
+Content-Type: text/javascript; charset=utf-8
+Cache-Control: max-age=3600
diff --git a/testing/web-platform/tests/resources/testharness.js b/testing/web-platform/tests/resources/testharness.js
new file mode 100644
index 0000000000..413993089b
--- /dev/null
+++ b/testing/web-platform/tests/resources/testharness.js
@@ -0,0 +1,4933 @@
+/*global self*/
+/*jshint latedef: nofunc*/
+
+/* Documentation: https://web-platform-tests.org/writing-tests/testharness-api.html
+ * (../docs/_writing-tests/testharness-api.md) */
+
+(function (global_scope)
+{
+ // default timeout is 10 seconds, test can override if needed
+ var settings = {
+ output:true,
+ harness_timeout:{
+ "normal":10000,
+ "long":60000
+ },
+ test_timeout:null,
+ message_events: ["start", "test_state", "result", "completion"],
+ debug: false,
+ };
+
+ var xhtml_ns = "http://www.w3.org/1999/xhtml";
+
+ /*
+ * TestEnvironment is an abstraction for the environment in which the test
+ * harness is used. Each implementation of a test environment has to provide
+ * the following interface:
+ *
+ * interface TestEnvironment {
+ * // Invoked after the global 'tests' object has been created and it's
+ * // safe to call add_*_callback() to register event handlers.
+ * void on_tests_ready();
+ *
+ * // Invoked after setup() has been called to notify the test environment
+ * // of changes to the test harness properties.
+ * void on_new_harness_properties(object properties);
+ *
+ * // Should return a new unique default test name.
+ * DOMString next_default_test_name();
+ *
+ * // Should return the test harness timeout duration in milliseconds.
+ * float test_timeout();
+ * };
+ */
+
+ /*
+ * A test environment with a DOM. The global object is 'window'. By default
+ * test results are displayed in a table. Any parent windows receive
+ * callbacks or messages via postMessage() when test events occur. See
+ * apisample11.html and apisample12.html.
+ */
+ function WindowTestEnvironment() {
+ this.name_counter = 0;
+ this.window_cache = null;
+ this.output_handler = null;
+ this.all_loaded = false;
+ var this_obj = this;
+ this.message_events = [];
+ this.dispatched_messages = [];
+
+ this.message_functions = {
+ start: [add_start_callback, remove_start_callback,
+ function (properties) {
+ this_obj._dispatch("start_callback", [properties],
+ {type: "start", properties: properties});
+ }],
+
+ test_state: [add_test_state_callback, remove_test_state_callback,
+ function(test) {
+ this_obj._dispatch("test_state_callback", [test],
+ {type: "test_state",
+ test: test.structured_clone()});
+ }],
+ result: [add_result_callback, remove_result_callback,
+ function (test) {
+ this_obj.output_handler.show_status();
+ this_obj._dispatch("result_callback", [test],
+ {type: "result",
+ test: test.structured_clone()});
+ }],
+ completion: [add_completion_callback, remove_completion_callback,
+ function (tests, harness_status, asserts) {
+ var cloned_tests = map(tests, function(test) {
+ return test.structured_clone();
+ });
+ this_obj._dispatch("completion_callback", [tests, harness_status],
+ {type: "complete",
+ tests: cloned_tests,
+ status: harness_status.structured_clone(),
+ asserts: asserts.map(assert => assert.structured_clone())});
+ }]
+ }
+
+ on_event(window, 'load', function() {
+ this_obj.all_loaded = true;
+ });
+
+ on_event(window, 'message', function(event) {
+ if (event.data && event.data.type === "getmessages" && event.source) {
+ // A window can post "getmessages" to receive a duplicate of every
+ // message posted by this environment so far. This allows subscribers
+ // from fetch_tests_from_window to 'catch up' to the current state of
+ // this environment.
+ for (var i = 0; i < this_obj.dispatched_messages.length; ++i)
+ {
+ event.source.postMessage(this_obj.dispatched_messages[i], "*");
+ }
+ }
+ });
+ }
+
+ WindowTestEnvironment.prototype._dispatch = function(selector, callback_args, message_arg) {
+ this.dispatched_messages.push(message_arg);
+ this._forEach_windows(
+ function(w, same_origin) {
+ if (same_origin) {
+ try {
+ var has_selector = selector in w;
+ } catch(e) {
+ // If document.domain was set at some point same_origin can be
+ // wrong and the above will fail.
+ has_selector = false;
+ }
+ if (has_selector) {
+ try {
+ w[selector].apply(undefined, callback_args);
+ } catch (e) {}
+ }
+ }
+ if (w !== self) {
+ w.postMessage(message_arg, "*");
+ }
+ });
+ };
+
+ WindowTestEnvironment.prototype._forEach_windows = function(callback) {
+ // Iterate over the windows [self ... top, opener]. The callback is passed
+ // two objects, the first one is the window object itself, the second one
+ // is a boolean indicating whether or not it's on the same origin as the
+ // current window.
+ var cache = this.window_cache;
+ if (!cache) {
+ cache = [[self, true]];
+ var w = self;
+ var i = 0;
+ var so;
+ while (w != w.parent) {
+ w = w.parent;
+ so = is_same_origin(w);
+ cache.push([w, so]);
+ i++;
+ }
+ w = window.opener;
+ if (w) {
+ cache.push([w, is_same_origin(w)]);
+ }
+ this.window_cache = cache;
+ }
+
+ forEach(cache,
+ function(a) {
+ callback.apply(null, a);
+ });
+ };
+
+ WindowTestEnvironment.prototype.on_tests_ready = function() {
+ var output = new Output();
+ this.output_handler = output;
+
+ var this_obj = this;
+
+ add_start_callback(function (properties) {
+ this_obj.output_handler.init(properties);
+ });
+
+ add_test_state_callback(function(test) {
+ this_obj.output_handler.show_status();
+ });
+
+ add_result_callback(function (test) {
+ this_obj.output_handler.show_status();
+ });
+
+ add_completion_callback(function (tests, harness_status, asserts_run) {
+ this_obj.output_handler.show_results(tests, harness_status, asserts_run);
+ });
+ this.setup_messages(settings.message_events);
+ };
+
+ WindowTestEnvironment.prototype.setup_messages = function(new_events) {
+ var this_obj = this;
+ forEach(settings.message_events, function(x) {
+ var current_dispatch = this_obj.message_events.indexOf(x) !== -1;
+ var new_dispatch = new_events.indexOf(x) !== -1;
+ if (!current_dispatch && new_dispatch) {
+ this_obj.message_functions[x][0](this_obj.message_functions[x][2]);
+ } else if (current_dispatch && !new_dispatch) {
+ this_obj.message_functions[x][1](this_obj.message_functions[x][2]);
+ }
+ });
+ this.message_events = new_events;
+ }
+
+ WindowTestEnvironment.prototype.next_default_test_name = function() {
+ var suffix = this.name_counter > 0 ? " " + this.name_counter : "";
+ this.name_counter++;
+ return get_title() + suffix;
+ };
+
+ WindowTestEnvironment.prototype.on_new_harness_properties = function(properties) {
+ this.output_handler.setup(properties);
+ if (properties.hasOwnProperty("message_events")) {
+ this.setup_messages(properties.message_events);
+ }
+ };
+
+ WindowTestEnvironment.prototype.add_on_loaded_callback = function(callback) {
+ on_event(window, 'load', callback);
+ };
+
+ WindowTestEnvironment.prototype.test_timeout = function() {
+ var metas = document.getElementsByTagName("meta");
+ for (var i = 0; i < metas.length; i++) {
+ if (metas[i].name == "timeout") {
+ if (metas[i].content == "long") {
+ return settings.harness_timeout.long;
+ }
+ break;
+ }
+ }
+ return settings.harness_timeout.normal;
+ };
+
+ /*
+ * Base TestEnvironment implementation for a generic web worker.
+ *
+ * Workers accumulate test results. One or more clients can connect and
+ * retrieve results from a worker at any time.
+ *
+ * WorkerTestEnvironment supports communicating with a client via a
+ * MessagePort. The mechanism for determining the appropriate MessagePort
+ * for communicating with a client depends on the type of worker and is
+ * implemented by the various specializations of WorkerTestEnvironment
+ * below.
+ *
+ * A client document using testharness can use fetch_tests_from_worker() to
+ * retrieve results from a worker. See apisample16.html.
+ */
+ function WorkerTestEnvironment() {
+ this.name_counter = 0;
+ this.all_loaded = true;
+ this.message_list = [];
+ this.message_ports = [];
+ }
+
+ WorkerTestEnvironment.prototype._dispatch = function(message) {
+ this.message_list.push(message);
+ for (var i = 0; i < this.message_ports.length; ++i)
+ {
+ this.message_ports[i].postMessage(message);
+ }
+ };
+
+ // The only requirement is that port has a postMessage() method. It doesn't
+ // have to be an instance of a MessagePort, and often isn't.
+ WorkerTestEnvironment.prototype._add_message_port = function(port) {
+ this.message_ports.push(port);
+ for (var i = 0; i < this.message_list.length; ++i)
+ {
+ port.postMessage(this.message_list[i]);
+ }
+ };
+
+ WorkerTestEnvironment.prototype.next_default_test_name = function() {
+ var suffix = this.name_counter > 0 ? " " + this.name_counter : "";
+ this.name_counter++;
+ return get_title() + suffix;
+ };
+
+ WorkerTestEnvironment.prototype.on_new_harness_properties = function() {};
+
+ WorkerTestEnvironment.prototype.on_tests_ready = function() {
+ var this_obj = this;
+ add_start_callback(
+ function(properties) {
+ this_obj._dispatch({
+ type: "start",
+ properties: properties,
+ });
+ });
+ add_test_state_callback(
+ function(test) {
+ this_obj._dispatch({
+ type: "test_state",
+ test: test.structured_clone()
+ });
+ });
+ add_result_callback(
+ function(test) {
+ this_obj._dispatch({
+ type: "result",
+ test: test.structured_clone()
+ });
+ });
+ add_completion_callback(
+ function(tests, harness_status, asserts) {
+ this_obj._dispatch({
+ type: "complete",
+ tests: map(tests,
+ function(test) {
+ return test.structured_clone();
+ }),
+ status: harness_status.structured_clone(),
+ asserts: asserts.map(assert => assert.structured_clone()),
+ });
+ });
+ };
+
+ WorkerTestEnvironment.prototype.add_on_loaded_callback = function() {};
+
+ WorkerTestEnvironment.prototype.test_timeout = function() {
+ // Tests running in a worker don't have a default timeout. I.e. all
+ // worker tests behave as if settings.explicit_timeout is true.
+ return null;
+ };
+
+ /*
+ * Dedicated web workers.
+ * https://html.spec.whatwg.org/multipage/workers.html#dedicatedworkerglobalscope
+ *
+ * This class is used as the test_environment when testharness is running
+ * inside a dedicated worker.
+ */
+ function DedicatedWorkerTestEnvironment() {
+ WorkerTestEnvironment.call(this);
+ // self is an instance of DedicatedWorkerGlobalScope which exposes
+ // a postMessage() method for communicating via the message channel
+ // established when the worker is created.
+ this._add_message_port(self);
+ }
+ DedicatedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype);
+
+ DedicatedWorkerTestEnvironment.prototype.on_tests_ready = function() {
+ WorkerTestEnvironment.prototype.on_tests_ready.call(this);
+ // In the absence of an onload notification, we a require dedicated
+ // workers to explicitly signal when the tests are done.
+ tests.wait_for_finish = true;
+ };
+
+ /*
+ * Shared web workers.
+ * https://html.spec.whatwg.org/multipage/workers.html#sharedworkerglobalscope
+ *
+ * This class is used as the test_environment when testharness is running
+ * inside a shared web worker.
+ */
+ function SharedWorkerTestEnvironment() {
+ WorkerTestEnvironment.call(this);
+ var this_obj = this;
+ // Shared workers receive message ports via the 'onconnect' event for
+ // each connection.
+ self.addEventListener("connect",
+ function(message_event) {
+ this_obj._add_message_port(message_event.source);
+ }, false);
+ }
+ SharedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype);
+
+ SharedWorkerTestEnvironment.prototype.on_tests_ready = function() {
+ WorkerTestEnvironment.prototype.on_tests_ready.call(this);
+ // In the absence of an onload notification, we a require shared
+ // workers to explicitly signal when the tests are done.
+ tests.wait_for_finish = true;
+ };
+
+ /*
+ * Service workers.
+ * http://www.w3.org/TR/service-workers/
+ *
+ * This class is used as the test_environment when testharness is running
+ * inside a service worker.
+ */
+ function ServiceWorkerTestEnvironment() {
+ WorkerTestEnvironment.call(this);
+ this.all_loaded = false;
+ this.on_loaded_callback = null;
+ var this_obj = this;
+ self.addEventListener("message",
+ function(event) {
+ if (event.data && event.data.type && event.data.type === "connect") {
+ this_obj._add_message_port(event.source);
+ }
+ }, false);
+
+ // The oninstall event is received after the service worker script and
+ // all imported scripts have been fetched and executed. It's the
+ // equivalent of an onload event for a document. All tests should have
+ // been added by the time this event is received, thus it's not
+ // necessary to wait until the onactivate event. However, tests for
+ // installed service workers need another event which is equivalent to
+ // the onload event because oninstall is fired only on installation. The
+ // onmessage event is used for that purpose since tests using
+ // testharness.js should ask the result to its service worker by
+ // PostMessage. If the onmessage event is triggered on the service
+ // worker's context, that means the worker's script has been evaluated.
+ on_event(self, "install", on_all_loaded);
+ on_event(self, "message", on_all_loaded);
+ function on_all_loaded() {
+ if (this_obj.all_loaded)
+ return;
+ this_obj.all_loaded = true;
+ if (this_obj.on_loaded_callback) {
+ this_obj.on_loaded_callback();
+ }
+ }
+ }
+
+ ServiceWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype);
+
+ ServiceWorkerTestEnvironment.prototype.add_on_loaded_callback = function(callback) {
+ if (this.all_loaded) {
+ callback();
+ } else {
+ this.on_loaded_callback = callback;
+ }
+ };
+
+ /*
+ * Shadow realms.
+ * https://github.com/tc39/proposal-shadowrealm
+ *
+ * This class is used as the test_environment when testharness is running
+ * inside a shadow realm.
+ */
+ function ShadowRealmTestEnvironment() {
+ WorkerTestEnvironment.call(this);
+ this.all_loaded = false;
+ this.on_loaded_callback = null;
+ }
+
+ ShadowRealmTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype);
+
+ /**
+ * Signal to the test environment that the tests are ready and the on-loaded
+ * callback should be run.
+ *
+ * Shadow realms are not *really* a DOM context: they have no `onload` or similar
+ * event for us to use to set up the test environment; so, instead, this method
+ * is manually triggered from the incubating realm
+ *
+ * @param {Function} message_destination - a function that receives JSON-serializable
+ * data to send to the incubating realm, in the same format as used by RemoteContext
+ */
+ ShadowRealmTestEnvironment.prototype.begin = function(message_destination) {
+ if (this.all_loaded) {
+ throw new Error("Tried to start a shadow realm test environment after it has already started");
+ }
+ var fakeMessagePort = {};
+ fakeMessagePort.postMessage = message_destination;
+ this._add_message_port(fakeMessagePort);
+ this.all_loaded = true;
+ if (this.on_loaded_callback) {
+ this.on_loaded_callback();
+ }
+ };
+
+ ShadowRealmTestEnvironment.prototype.add_on_loaded_callback = function(callback) {
+ if (this.all_loaded) {
+ callback();
+ } else {
+ this.on_loaded_callback = callback;
+ }
+ };
+
+ /*
+ * JavaScript shells.
+ *
+ * This class is used as the test_environment when testharness is running
+ * inside a JavaScript shell.
+ */
+ function ShellTestEnvironment() {
+ this.name_counter = 0;
+ this.all_loaded = false;
+ this.on_loaded_callback = null;
+ Promise.resolve().then(function() {
+ this.all_loaded = true
+ if (this.on_loaded_callback) {
+ this.on_loaded_callback();
+ }
+ }.bind(this));
+ this.message_list = [];
+ this.message_ports = [];
+ }
+
+ ShellTestEnvironment.prototype.next_default_test_name = function() {
+ var suffix = this.name_counter > 0 ? " " + this.name_counter : "";
+ this.name_counter++;
+ return get_title() + suffix;
+ };
+
+ ShellTestEnvironment.prototype.on_new_harness_properties = function() {};
+
+ ShellTestEnvironment.prototype.on_tests_ready = function() {};
+
+ ShellTestEnvironment.prototype.add_on_loaded_callback = function(callback) {
+ if (this.all_loaded) {
+ callback();
+ } else {
+ this.on_loaded_callback = callback;
+ }
+ };
+
+ ShellTestEnvironment.prototype.test_timeout = function() {
+ // Tests running in a shell don't have a default timeout, so behave as
+ // if settings.explicit_timeout is true.
+ return null;
+ };
+
+ function create_test_environment() {
+ if ('document' in global_scope) {
+ return new WindowTestEnvironment();
+ }
+ if ('DedicatedWorkerGlobalScope' in global_scope &&
+ global_scope instanceof DedicatedWorkerGlobalScope) {
+ return new DedicatedWorkerTestEnvironment();
+ }
+ if ('SharedWorkerGlobalScope' in global_scope &&
+ global_scope instanceof SharedWorkerGlobalScope) {
+ return new SharedWorkerTestEnvironment();
+ }
+ if ('ServiceWorkerGlobalScope' in global_scope &&
+ global_scope instanceof ServiceWorkerGlobalScope) {
+ return new ServiceWorkerTestEnvironment();
+ }
+ if ('WorkerGlobalScope' in global_scope &&
+ global_scope instanceof WorkerGlobalScope) {
+ return new DedicatedWorkerTestEnvironment();
+ }
+ /* Shadow realm global objects are _ordinary_ objects (i.e. their prototype is
+ * Object) so we don't have a nice `instanceof` test to use; instead, we
+ * check if the there is a GLOBAL.isShadowRealm() property
+ * on the global object. that was set by the test harness when it
+ * created the ShadowRealm.
+ */
+ if (global_scope.GLOBAL && global_scope.GLOBAL.isShadowRealm()) {
+ return new ShadowRealmTestEnvironment();
+ }
+
+ return new ShellTestEnvironment();
+ }
+
+ var test_environment = create_test_environment();
+
+ function is_shared_worker(worker) {
+ return 'SharedWorker' in global_scope && worker instanceof SharedWorker;
+ }
+
+ function is_service_worker(worker) {
+ // The worker object may be from another execution context,
+ // so do not use instanceof here.
+ return 'ServiceWorker' in global_scope &&
+ Object.prototype.toString.call(worker) == '[object ServiceWorker]';
+ }
+
+ var seen_func_name = Object.create(null);
+
+ function get_test_name(func, name)
+ {
+ if (name) {
+ return name;
+ }
+
+ if (func) {
+ var func_code = func.toString();
+
+ // Try and match with brackets, but fallback to matching without
+ var arrow = func_code.match(/^\(\)\s*=>\s*(?:{(.*)}\s*|(.*))$/);
+
+ // Check for JS line separators
+ if (arrow !== null && !/[\u000A\u000D\u2028\u2029]/.test(func_code)) {
+ var trimmed = (arrow[1] !== undefined ? arrow[1] : arrow[2]).trim();
+ // drop trailing ; if there's no earlier ones
+ trimmed = trimmed.replace(/^([^;]*)(;\s*)+$/, "$1");
+
+ if (trimmed) {
+ let name = trimmed;
+ if (seen_func_name[trimmed]) {
+ // This subtest name already exists, so add a suffix.
+ name += " " + seen_func_name[trimmed];
+ } else {
+ seen_func_name[trimmed] = 0;
+ }
+ seen_func_name[trimmed] += 1;
+ return name;
+ }
+ }
+ }
+
+ return test_environment.next_default_test_name();
+ }
+
+ /**
+ * @callback TestFunction
+ * @param {Test} test - The test currnetly being run.
+ * @param {Any[]} args - Additional args to pass to function.
+ *
+ */
+
+ /**
+ * Create a synchronous test
+ *
+ * @param {TestFunction} func - Test function. This is executed
+ * immediately. If it returns without error, the test status is
+ * set to ``PASS``. If it throws an :js:class:`AssertionError`, or
+ * any other exception, the test status is set to ``FAIL``
+ * (typically from an `assert` function).
+ * @param {String} name - Test name. This must be unique in a
+ * given file and must be invariant between runs.
+ */
+ function test(func, name, properties)
+ {
+ if (tests.promise_setup_called) {
+ tests.status.status = tests.status.ERROR;
+ tests.status.message = '`test` invoked after `promise_setup`';
+ tests.complete();
+ }
+ var test_name = get_test_name(func, name);
+ var test_obj = new Test(test_name, properties);
+ var value = test_obj.step(func, test_obj, test_obj);
+
+ if (value !== undefined) {
+ var msg = 'Test named "' + test_name +
+ '" passed a function to `test` that returned a value.';
+
+ try {
+ if (value && typeof value.then === 'function') {
+ msg += ' Consider using `promise_test` instead when ' +
+ 'using Promises or async/await.';
+ }
+ } catch (err) {}
+
+ tests.status.status = tests.status.ERROR;
+ tests.status.message = msg;
+ }
+
+ if (test_obj.phase === test_obj.phases.STARTED) {
+ test_obj.done();
+ }
+ }
+
+ /**
+ * Create an asynchronous test
+ *
+ * @param {TestFunction|string} funcOrName - Initial step function
+ * to call immediately with the test name as an argument (if any),
+ * or name of the test.
+ * @param {String} name - Test name (if a test function was
+ * provided). This must be unique in a given file and must be
+ * invariant between runs.
+ * @returns {Test} An object representing the ongoing test.
+ */
+ function async_test(func, name, properties)
+ {
+ if (tests.promise_setup_called) {
+ tests.status.status = tests.status.ERROR;
+ tests.status.message = '`async_test` invoked after `promise_setup`';
+ tests.complete();
+ }
+ if (typeof func !== "function") {
+ properties = name;
+ name = func;
+ func = null;
+ }
+ var test_name = get_test_name(func, name);
+ var test_obj = new Test(test_name, properties);
+ if (func) {
+ var value = test_obj.step(func, test_obj, test_obj);
+
+ // Test authors sometimes return values to async_test, expecting us
+ // to handle the value somehow. Make doing so a harness error to be
+ // clear this is invalid, and point authors to promise_test if it
+ // may be appropriate.
+ //
+ // Note that we only perform this check on the initial function
+ // passed to async_test, not on any later steps - we haven't seen a
+ // consistent problem with those (and it's harder to check).
+ if (value !== undefined) {
+ var msg = 'Test named "' + test_name +
+ '" passed a function to `async_test` that returned a value.';
+
+ try {
+ if (value && typeof value.then === 'function') {
+ msg += ' Consider using `promise_test` instead when ' +
+ 'using Promises or async/await.';
+ }
+ } catch (err) {}
+
+ tests.set_status(tests.status.ERROR, msg);
+ tests.complete();
+ }
+ }
+ return test_obj;
+ }
+
+ /**
+ * Create a promise test.
+ *
+ * Promise tests are tests which are represented by a promise
+ * object. If the promise is fulfilled the test passes, if it's
+ * rejected the test fails, otherwise the test passes.
+ *
+ * @param {TestFunction} func - Test function. This must return a
+ * promise. The test is automatically marked as complete once the
+ * promise settles.
+ * @param {String} name - Test name. This must be unique in a
+ * given file and must be invariant between runs.
+ */
+ function promise_test(func, name, properties) {
+ if (typeof func !== "function") {
+ properties = name;
+ name = func;
+ func = null;
+ }
+ var test_name = get_test_name(func, name);
+ var test = new Test(test_name, properties);
+ test._is_promise_test = true;
+
+ // If there is no promise tests queue make one.
+ if (!tests.promise_tests) {
+ tests.promise_tests = Promise.resolve();
+ }
+ tests.promise_tests = tests.promise_tests.then(function() {
+ return new Promise(function(resolve) {
+ var promise = test.step(func, test, test);
+
+ test.step(function() {
+ assert(!!promise, "promise_test", null,
+ "test body must return a 'thenable' object (received ${value})",
+ {value:promise});
+ assert(typeof promise.then === "function", "promise_test", null,
+ "test body must return a 'thenable' object (received an object with no `then` method)",
+ null);
+ });
+
+ // Test authors may use the `step` method within a
+ // `promise_test` even though this reflects a mixture of
+ // asynchronous control flow paradigms. The "done" callback
+ // should be registered prior to the resolution of the
+ // user-provided Promise to avoid timeouts in cases where the
+ // Promise does not settle but a `step` function has thrown an
+ // error.
+ add_test_done_callback(test, resolve);
+
+ Promise.resolve(promise)
+ .catch(test.step_func(
+ function(value) {
+ if (value instanceof AssertionError) {
+ throw value;
+ }
+ assert(false, "promise_test", null,
+ "Unhandled rejection with value: ${value}", {value:value});
+ }))
+ .then(function() {
+ test.done();
+ });
+ });
+ });
+ }
+
+ /**
+ * Make a copy of a Promise in the current realm.
+ *
+ * @param {Promise} promise the given promise that may be from a different
+ * realm
+ * @returns {Promise}
+ *
+ * An arbitrary promise provided by the caller may have originated
+ * in another frame that have since navigated away, rendering the
+ * frame's document inactive. Such a promise cannot be used with
+ * `await` or Promise.resolve(), as microtasks associated with it
+ * may be prevented from being run. See `issue
+ * 5319<https://github.com/whatwg/html/issues/5319>`_ for a
+ * particular case.
+ *
+ * In functions we define here, there is an expectation from the caller
+ * that the promise is from the current realm, that can always be used with
+ * `await`, etc. We therefore create a new promise in this realm that
+ * inherit the value and status from the given promise.
+ */
+
+ function bring_promise_to_current_realm(promise) {
+ return new Promise(promise.then.bind(promise));
+ }
+
+ /**
+ * Assert that a Promise is rejected with the right ECMAScript exception.
+ *
+ * @param {Test} test - the `Test` to use for the assertion.
+ * @param {Function} constructor - The expected exception constructor.
+ * @param {Promise} promise - The promise that's expected to
+ * reject with the given exception.
+ * @param {string} [description] Error message to add to assert in case of
+ * failure.
+ */
+ function promise_rejects_js(test, constructor, promise, description) {
+ return bring_promise_to_current_realm(promise)
+ .then(test.unreached_func("Should have rejected: " + description))
+ .catch(function(e) {
+ assert_throws_js_impl(constructor, function() { throw e },
+ description, "promise_rejects_js");
+ });
+ }
+
+ /**
+ * Assert that a Promise is rejected with the right DOMException.
+ *
+ * For the remaining arguments, there are two ways of calling
+ * promise_rejects_dom:
+ *
+ * 1) If the DOMException is expected to come from the current global, the
+ * third argument should be the promise expected to reject, and a fourth,
+ * optional, argument is the assertion description.
+ *
+ * 2) If the DOMException is expected to come from some other global, the
+ * third argument should be the DOMException constructor from that global,
+ * the fourth argument the promise expected to reject, and the fifth,
+ * optional, argument the assertion description.
+ *
+ * @param {Test} test - the `Test` to use for the assertion.
+ * @param {number|string} type - See documentation for
+ * `assert_throws_dom <#assert_throws_dom>`_.
+ * @param {Function} promiseOrConstructor - Either the constructor
+ * for the expected exception (if the exception comes from another
+ * global), or the promise that's expected to reject (if the
+ * exception comes from the current global).
+ * @param {Function|string} descriptionOrPromise - Either the
+ * promise that's expected to reject (if the exception comes from
+ * another global), or the optional description of the condition
+ * being tested (if the exception comes from the current global).
+ * @param {string} [description] - Description of the condition
+ * being tested (if the exception comes from another global).
+ *
+ */
+ function promise_rejects_dom(test, type, promiseOrConstructor, descriptionOrPromise, maybeDescription) {
+ let constructor, promise, description;
+ if (typeof promiseOrConstructor === "function" &&
+ promiseOrConstructor.name === "DOMException") {
+ constructor = promiseOrConstructor;
+ promise = descriptionOrPromise;
+ description = maybeDescription;
+ } else {
+ constructor = self.DOMException;
+ promise = promiseOrConstructor;
+ description = descriptionOrPromise;
+ assert(maybeDescription === undefined,
+ "Too many args pased to no-constructor version of promise_rejects_dom");
+ }
+ return bring_promise_to_current_realm(promise)
+ .then(test.unreached_func("Should have rejected: " + description))
+ .catch(function(e) {
+ assert_throws_dom_impl(type, function() { throw e }, description,
+ "promise_rejects_dom", constructor);
+ });
+ }
+
+ /**
+ * Assert that a Promise is rejected with the provided value.
+ *
+ * @param {Test} test - the `Test` to use for the assertion.
+ * @param {Any} exception - The expected value of the rejected promise.
+ * @param {Promise} promise - The promise that's expected to
+ * reject.
+ * @param {string} [description] Error message to add to assert in case of
+ * failure.
+ */
+ function promise_rejects_exactly(test, exception, promise, description) {
+ return bring_promise_to_current_realm(promise)
+ .then(test.unreached_func("Should have rejected: " + description))
+ .catch(function(e) {
+ assert_throws_exactly_impl(exception, function() { throw e },
+ description, "promise_rejects_exactly");
+ });
+ }
+
+ /**
+ * Allow DOM events to be handled using Promises.
+ *
+ * This can make it a lot easier to test a very specific series of events,
+ * including ensuring that unexpected events are not fired at any point.
+ *
+ * `EventWatcher` will assert if an event occurs while there is no `wait_for`
+ * created Promise waiting to be fulfilled, or if the event is of a different type
+ * to the type currently expected. This ensures that only the events that are
+ * expected occur, in the correct order, and with the correct timing.
+ *
+ * @constructor
+ * @param {Test} test - The `Test` to use for the assertion.
+ * @param {EventTarget} watchedNode - The target expected to receive the events.
+ * @param {string[]} eventTypes - List of events to watch for.
+ * @param {Promise} timeoutPromise - Promise that will cause the
+ * test to be set to `TIMEOUT` once fulfilled.
+ *
+ */
+ function EventWatcher(test, watchedNode, eventTypes, timeoutPromise)
+ {
+ if (typeof eventTypes == 'string') {
+ eventTypes = [eventTypes];
+ }
+
+ var waitingFor = null;
+
+ // This is null unless we are recording all events, in which case it
+ // will be an Array object.
+ var recordedEvents = null;
+
+ var eventHandler = test.step_func(function(evt) {
+ assert_true(!!waitingFor,
+ 'Not expecting event, but got ' + evt.type + ' event');
+ assert_equals(evt.type, waitingFor.types[0],
+ 'Expected ' + waitingFor.types[0] + ' event, but got ' +
+ evt.type + ' event instead');
+
+ if (Array.isArray(recordedEvents)) {
+ recordedEvents.push(evt);
+ }
+
+ if (waitingFor.types.length > 1) {
+ // Pop first event from array
+ waitingFor.types.shift();
+ return;
+ }
+ // We need to null out waitingFor before calling the resolve function
+ // since the Promise's resolve handlers may call wait_for() which will
+ // need to set waitingFor.
+ var resolveFunc = waitingFor.resolve;
+ waitingFor = null;
+ // Likewise, we should reset the state of recordedEvents.
+ var result = recordedEvents || evt;
+ recordedEvents = null;
+ resolveFunc(result);
+ });
+
+ for (var i = 0; i < eventTypes.length; i++) {
+ watchedNode.addEventListener(eventTypes[i], eventHandler, false);
+ }
+
+ /**
+ * Returns a Promise that will resolve after the specified event or
+ * series of events has occurred.
+ *
+ * @param {Object} options An optional options object. If the 'record' property
+ * on this object has the value 'all', when the Promise
+ * returned by this function is resolved, *all* Event
+ * objects that were waited for will be returned as an
+ * array.
+ *
+ * @example
+ * const watcher = new EventWatcher(t, div, [ 'animationstart',
+ * 'animationiteration',
+ * 'animationend' ]);
+ * return watcher.wait_for([ 'animationstart', 'animationend' ],
+ * { record: 'all' }).then(evts => {
+ * assert_equals(evts[0].elapsedTime, 0.0);
+ * assert_equals(evts[1].elapsedTime, 2.0);
+ * });
+ */
+ this.wait_for = function(types, options) {
+ if (waitingFor) {
+ return Promise.reject('Already waiting for an event or events');
+ }
+ if (typeof types == 'string') {
+ types = [types];
+ }
+ if (options && options.record && options.record === 'all') {
+ recordedEvents = [];
+ }
+ return new Promise(function(resolve, reject) {
+ var timeout = test.step_func(function() {
+ // If the timeout fires after the events have been received
+ // or during a subsequent call to wait_for, ignore it.
+ if (!waitingFor || waitingFor.resolve !== resolve)
+ return;
+
+ // This should always fail, otherwise we should have
+ // resolved the promise.
+ assert_true(waitingFor.types.length == 0,
+ 'Timed out waiting for ' + waitingFor.types.join(', '));
+ var result = recordedEvents;
+ recordedEvents = null;
+ var resolveFunc = waitingFor.resolve;
+ waitingFor = null;
+ resolveFunc(result);
+ });
+
+ if (timeoutPromise) {
+ timeoutPromise().then(timeout);
+ }
+
+ waitingFor = {
+ types: types,
+ resolve: resolve,
+ reject: reject
+ };
+ });
+ };
+
+ /**
+ * Stop listening for events
+ */
+ function stop_watching() {
+ for (var i = 0; i < eventTypes.length; i++) {
+ watchedNode.removeEventListener(eventTypes[i], eventHandler, false);
+ }
+ };
+
+ test._add_cleanup(stop_watching);
+
+ return this;
+ }
+ expose(EventWatcher, 'EventWatcher');
+
+ /**
+ * @typedef {Object} SettingsObject
+ * @property {bool} single_test - Use the single-page-test
+ * mode. In this mode the Document represents a single
+ * `async_test`. Asserts may be used directly without requiring
+ * `Test.step` or similar wrappers, and any exceptions set the
+ * status of the test rather than the status of the harness.
+ * @property {bool} allow_uncaught_exception - don't treat an
+ * uncaught exception as an error; needed when e.g. testing the
+ * `window.onerror` handler.
+ * @property {boolean} explicit_done - Wait for a call to `done()`
+ * before declaring all tests complete (this is always true for
+ * single-page tests).
+ * @property hide_test_state - hide the test state output while
+ * the test is running; This is helpful when the output of the test state
+ * may interfere the test results.
+ * @property {bool} explicit_timeout - disable file timeout; only
+ * stop waiting for results when the `timeout()` function is
+ * called This should typically only be set for manual tests, or
+ * by a test runner that providees its own timeout mechanism.
+ * @property {number} timeout_multiplier - Multiplier to apply to
+ * per-test timeouts. This should only be set by a test runner.
+ * @property {Document} output_document - The document to which
+ * results should be logged. By default this is the current
+ * document but could be an ancestor document in some cases e.g. a
+ * SVG test loaded in an HTML wrapper
+ *
+ */
+
+ /**
+ * Configure the harness
+ *
+ * @param {Function|SettingsObject} funcOrProperties - Either a
+ * setup function to run, or a set of properties. If this is a
+ * function that function is run synchronously. Any exception in
+ * the function will set the overall harness status to `ERROR`.
+ * @param {SettingsObject} maybeProperties - An object containing
+ * the settings to use, if the first argument is a function.
+ *
+ */
+ function setup(func_or_properties, maybe_properties)
+ {
+ var func = null;
+ var properties = {};
+ if (arguments.length === 2) {
+ func = func_or_properties;
+ properties = maybe_properties;
+ } else if (func_or_properties instanceof Function) {
+ func = func_or_properties;
+ } else {
+ properties = func_or_properties;
+ }
+ tests.setup(func, properties);
+ test_environment.on_new_harness_properties(properties);
+ }
+
+ /**
+ * Configure the harness, waiting for a promise to resolve
+ * before running any `promise_test` tests.
+ *
+ * @param {Function} func - Function returning a promise that's
+ * run synchronously. Promise tests are not run until after this
+ * function has resolved.
+ * @param {SettingsObject} [properties] - An object containing
+ * the harness settings to use.
+ *
+ */
+ function promise_setup(func, properties={})
+ {
+ if (typeof func !== "function") {
+ tests.set_status(tests.status.ERROR,
+ "promise_test invoked without a function");
+ tests.complete();
+ return;
+ }
+ tests.promise_setup_called = true;
+
+ if (!tests.promise_tests) {
+ tests.promise_tests = Promise.resolve();
+ }
+
+ tests.promise_tests = tests.promise_tests
+ .then(function()
+ {
+ var result;
+
+ tests.setup(null, properties);
+ result = func();
+ test_environment.on_new_harness_properties(properties);
+
+ if (!result || typeof result.then !== "function") {
+ throw "Non-thenable returned by function passed to `promise_setup`";
+ }
+ return result;
+ })
+ .catch(function(e)
+ {
+ tests.set_status(tests.status.ERROR,
+ String(e),
+ e && e.stack);
+ tests.complete();
+ });
+ }
+
+ /**
+ * Mark test loading as complete.
+ *
+ * Typically this function is called implicitly on page load; it's
+ * only necessary for users to call this when either the
+ * ``explicit_done`` or ``single_page`` properties have been set
+ * via the :js:func:`setup` function.
+ *
+ * For single page tests this marks the test as complete and sets its status.
+ * For other tests, this marks test loading as complete, but doesn't affect ongoing tests.
+ */
+ function done() {
+ if (tests.tests.length === 0) {
+ // `done` is invoked after handling uncaught exceptions, so if the
+ // harness status is already set, the corresponding message is more
+ // descriptive than the generic message defined here.
+ if (tests.status.status === null) {
+ tests.status.status = tests.status.ERROR;
+ tests.status.message = "done() was called without first defining any tests";
+ }
+
+ tests.complete();
+ return;
+ }
+ if (tests.file_is_test) {
+ // file is test files never have asynchronous cleanup logic,
+ // meaning the fully-synchronous `done` function can be used here.
+ tests.tests[0].done();
+ }
+ tests.end_wait();
+ }
+
+ /**
+ * @deprecated generate a list of tests from a function and list of arguments
+ *
+ * This is deprecated because it runs all the tests outside of the test functions
+ * and as a result any test throwing an exception will result in no tests being
+ * run. In almost all cases, you should simply call test within the loop you would
+ * use to generate the parameter list array.
+ *
+ * @param {Function} func - The function that will be called for each generated tests.
+ * @param {Any[][]} args - An array of arrays. Each nested array
+ * has the structure `[testName, ...testArgs]`. For each of these nested arrays
+ * array, a test is generated with name `testName` and test function equivalent to
+ * `func(..testArgs)`.
+ */
+ function generate_tests(func, args, properties) {
+ forEach(args, function(x, i)
+ {
+ var name = x[0];
+ test(function()
+ {
+ func.apply(this, x.slice(1));
+ },
+ name,
+ Array.isArray(properties) ? properties[i] : properties);
+ });
+ }
+
+ /**
+ * @deprecated
+ *
+ * Register a function as a DOM event listener to the
+ * given object for the event bubbling phase.
+ *
+ * @param {EventTarget} object - Event target
+ * @param {string} event - Event name
+ * @param {Function} callback - Event handler.
+ */
+ function on_event(object, event, callback)
+ {
+ object.addEventListener(event, callback, false);
+ }
+
+ /**
+ * Global version of :js:func:`Test.step_timeout` for use in single page tests.
+ *
+ * @param {Function} func - Function to run after the timeout
+ * @param {number} timeout - Time in ms to wait before running the
+ * test step. The actual wait time is ``timeout`` x
+ * ``timeout_multiplier``.
+ */
+ function step_timeout(func, timeout) {
+ var outer_this = this;
+ var args = Array.prototype.slice.call(arguments, 2);
+ return setTimeout(function() {
+ func.apply(outer_this, args);
+ }, timeout * tests.timeout_multiplier);
+ }
+
+ expose(test, 'test');
+ expose(async_test, 'async_test');
+ expose(promise_test, 'promise_test');
+ expose(promise_rejects_js, 'promise_rejects_js');
+ expose(promise_rejects_dom, 'promise_rejects_dom');
+ expose(promise_rejects_exactly, 'promise_rejects_exactly');
+ expose(generate_tests, 'generate_tests');
+ expose(setup, 'setup');
+ expose(promise_setup, 'promise_setup');
+ expose(done, 'done');
+ expose(on_event, 'on_event');
+ expose(step_timeout, 'step_timeout');
+
+ /*
+ * Return a string truncated to the given length, with ... added at the end
+ * if it was longer.
+ */
+ function truncate(s, len)
+ {
+ if (s.length > len) {
+ return s.substring(0, len - 3) + "...";
+ }
+ return s;
+ }
+
+ /*
+ * Return true if object is probably a Node object.
+ */
+ function is_node(object)
+ {
+ // I use duck-typing instead of instanceof, because
+ // instanceof doesn't work if the node is from another window (like an
+ // iframe's contentWindow):
+ // http://www.w3.org/Bugs/Public/show_bug.cgi?id=12295
+ try {
+ var has_node_properties = ("nodeType" in object &&
+ "nodeName" in object &&
+ "nodeValue" in object &&
+ "childNodes" in object);
+ } catch (e) {
+ // We're probably cross-origin, which means we aren't a node
+ return false;
+ }
+
+ if (has_node_properties) {
+ try {
+ object.nodeType;
+ } catch (e) {
+ // The object is probably Node.prototype or another prototype
+ // object that inherits from it, and not a Node instance.
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ var replacements = {
+ "0": "0",
+ "1": "x01",
+ "2": "x02",
+ "3": "x03",
+ "4": "x04",
+ "5": "x05",
+ "6": "x06",
+ "7": "x07",
+ "8": "b",
+ "9": "t",
+ "10": "n",
+ "11": "v",
+ "12": "f",
+ "13": "r",
+ "14": "x0e",
+ "15": "x0f",
+ "16": "x10",
+ "17": "x11",
+ "18": "x12",
+ "19": "x13",
+ "20": "x14",
+ "21": "x15",
+ "22": "x16",
+ "23": "x17",
+ "24": "x18",
+ "25": "x19",
+ "26": "x1a",
+ "27": "x1b",
+ "28": "x1c",
+ "29": "x1d",
+ "30": "x1e",
+ "31": "x1f",
+ "0xfffd": "ufffd",
+ "0xfffe": "ufffe",
+ "0xffff": "uffff",
+ };
+
+ /**
+ * Convert a value to a nice, human-readable string
+ *
+ * When many JavaScript Object values are coerced to a String, the
+ * resulting value will be ``"[object Object]"``. This obscures
+ * helpful information, making the coerced value unsuitable for
+ * use in assertion messages, test names, and debugging
+ * statements. `format_value` produces more distinctive string
+ * representations of many kinds of objects, including arrays and
+ * the more important DOM Node types. It also translates String
+ * values containing control characters to include human-readable
+ * representations.
+ *
+ * @example
+ * // "Document node with 2 children"
+ * format_value(document);
+ * @example
+ * // "\"foo\\uffffbar\""
+ * format_value("foo\uffffbar");
+ * @example
+ * // "[-0, Infinity]"
+ * format_value([-0, Infinity]);
+ * @param {Any} val - The value to convert to a string.
+ * @returns {string} - A string representation of ``val``, optimised for human readability.
+ */
+ function format_value(val, seen)
+ {
+ if (!seen) {
+ seen = [];
+ }
+ if (typeof val === "object" && val !== null) {
+ if (seen.indexOf(val) >= 0) {
+ return "[...]";
+ }
+ seen.push(val);
+ }
+ if (Array.isArray(val)) {
+ let output = "[";
+ if (val.beginEllipsis !== undefined) {
+ output += "…, ";
+ }
+ output += val.map(function(x) {return format_value(x, seen);}).join(", ");
+ if (val.endEllipsis !== undefined) {
+ output += ", …";
+ }
+ return output + "]";
+ }
+
+ switch (typeof val) {
+ case "string":
+ val = val.replace(/\\/g, "\\\\");
+ for (var p in replacements) {
+ var replace = "\\" + replacements[p];
+ val = val.replace(RegExp(String.fromCharCode(p), "g"), replace);
+ }
+ return '"' + val.replace(/"/g, '\\"') + '"';
+ case "boolean":
+ case "undefined":
+ return String(val);
+ case "number":
+ // In JavaScript, -0 === 0 and String(-0) == "0", so we have to
+ // special-case.
+ if (val === -0 && 1/val === -Infinity) {
+ return "-0";
+ }
+ return String(val);
+ case "object":
+ if (val === null) {
+ return "null";
+ }
+
+ // Special-case Node objects, since those come up a lot in my tests. I
+ // ignore namespaces.
+ if (is_node(val)) {
+ switch (val.nodeType) {
+ case Node.ELEMENT_NODE:
+ var ret = "<" + val.localName;
+ for (var i = 0; i < val.attributes.length; i++) {
+ ret += " " + val.attributes[i].name + '="' + val.attributes[i].value + '"';
+ }
+ ret += ">" + val.innerHTML + "</" + val.localName + ">";
+ return "Element node " + truncate(ret, 60);
+ case Node.TEXT_NODE:
+ return 'Text node "' + truncate(val.data, 60) + '"';
+ case Node.PROCESSING_INSTRUCTION_NODE:
+ return "ProcessingInstruction node with target " + format_value(truncate(val.target, 60)) + " and data " + format_value(truncate(val.data, 60));
+ case Node.COMMENT_NODE:
+ return "Comment node <!--" + truncate(val.data, 60) + "-->";
+ case Node.DOCUMENT_NODE:
+ return "Document node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children");
+ case Node.DOCUMENT_TYPE_NODE:
+ return "DocumentType node";
+ case Node.DOCUMENT_FRAGMENT_NODE:
+ return "DocumentFragment node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children");
+ default:
+ return "Node object of unknown type";
+ }
+ }
+
+ /* falls through */
+ default:
+ try {
+ return typeof val + ' "' + truncate(String(val), 1000) + '"';
+ } catch(e) {
+ return ("[stringifying object threw " + String(e) +
+ " with type " + String(typeof e) + "]");
+ }
+ }
+ }
+ expose(format_value, "format_value");
+
+ /*
+ * Assertions
+ */
+
+ function expose_assert(f, name) {
+ function assert_wrapper(...args) {
+ let status = Test.statuses.TIMEOUT;
+ let stack = null;
+ let new_assert_index = null;
+ try {
+ if (settings.debug) {
+ console.debug("ASSERT", name, tests.current_test && tests.current_test.name, args);
+ }
+ if (tests.output) {
+ tests.set_assert(name, args);
+ // Remember the newly pushed assert's index, because `apply`
+ // below might push new asserts.
+ new_assert_index = tests.asserts_run.length - 1;
+ }
+ const rv = f.apply(undefined, args);
+ status = Test.statuses.PASS;
+ return rv;
+ } catch(e) {
+ status = Test.statuses.FAIL;
+ stack = e.stack ? e.stack : null;
+ throw e;
+ } finally {
+ if (tests.output && !stack) {
+ stack = get_stack();
+ }
+ if (tests.output) {
+ tests.set_assert_status(new_assert_index, status, stack);
+ }
+ }
+ }
+ expose(assert_wrapper, name);
+ }
+
+ /**
+ * Assert that ``actual`` is strictly true
+ *
+ * @param {Any} actual - Value that is asserted to be true
+ * @param {string} [description] - Description of the condition being tested
+ */
+ function assert_true(actual, description)
+ {
+ assert(actual === true, "assert_true", description,
+ "expected true got ${actual}", {actual:actual});
+ }
+ expose_assert(assert_true, "assert_true");
+
+ /**
+ * Assert that ``actual`` is strictly false
+ *
+ * @param {Any} actual - Value that is asserted to be false
+ * @param {string} [description] - Description of the condition being tested
+ */
+ function assert_false(actual, description)
+ {
+ assert(actual === false, "assert_false", description,
+ "expected false got ${actual}", {actual:actual});
+ }
+ expose_assert(assert_false, "assert_false");
+
+ function same_value(x, y) {
+ if (y !== y) {
+ //NaN case
+ return x !== x;
+ }
+ if (x === 0 && y === 0) {
+ //Distinguish +0 and -0
+ return 1/x === 1/y;
+ }
+ return x === y;
+ }
+
+ /**
+ * Assert that ``actual`` is the same value as ``expected``.
+ *
+ * For objects this compares by cobject identity; for primitives
+ * this distinguishes between 0 and -0, and has correct handling
+ * of NaN.
+ *
+ * @param {Any} actual - Test value.
+ * @param {Any} expected - Expected value.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_equals(actual, expected, description)
+ {
+ /*
+ * Test if two primitives are equal or two objects
+ * are the same object
+ */
+ if (typeof actual != typeof expected) {
+ assert(false, "assert_equals", description,
+ "expected (" + typeof expected + ") ${expected} but got (" + typeof actual + ") ${actual}",
+ {expected:expected, actual:actual});
+ return;
+ }
+ assert(same_value(actual, expected), "assert_equals", description,
+ "expected ${expected} but got ${actual}",
+ {expected:expected, actual:actual});
+ }
+ expose_assert(assert_equals, "assert_equals");
+
+ /**
+ * Assert that ``actual`` is not the same value as ``expected``.
+ *
+ * Comparison is as for :js:func:`assert_equals`.
+ *
+ * @param {Any} actual - Test value.
+ * @param {Any} expected - The value ``actual`` is expected to be different to.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_not_equals(actual, expected, description)
+ {
+ assert(!same_value(actual, expected), "assert_not_equals", description,
+ "got disallowed value ${actual}",
+ {actual:actual});
+ }
+ expose_assert(assert_not_equals, "assert_not_equals");
+
+ /**
+ * Assert that ``expected`` is an array and ``actual`` is one of the members.
+ * This is implemented using ``indexOf``, so doesn't handle NaN or ±0 correctly.
+ *
+ * @param {Any} actual - Test value.
+ * @param {Array} expected - An array that ``actual`` is expected to
+ * be a member of.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_in_array(actual, expected, description)
+ {
+ assert(expected.indexOf(actual) != -1, "assert_in_array", description,
+ "value ${actual} not in array ${expected}",
+ {actual:actual, expected:expected});
+ }
+ expose_assert(assert_in_array, "assert_in_array");
+
+ // This function was deprecated in July of 2015.
+ // See https://github.com/web-platform-tests/wpt/issues/2033
+ /**
+ * @deprecated
+ * Recursively compare two objects for equality.
+ *
+ * See `Issue 2033
+ * <https://github.com/web-platform-tests/wpt/issues/2033>`_ for
+ * more information.
+ *
+ * @param {Object} actual - Test value.
+ * @param {Object} expected - Expected value.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_object_equals(actual, expected, description)
+ {
+ assert(typeof actual === "object" && actual !== null, "assert_object_equals", description,
+ "value is ${actual}, expected object",
+ {actual: actual});
+ //This needs to be improved a great deal
+ function check_equal(actual, expected, stack)
+ {
+ stack.push(actual);
+
+ var p;
+ for (p in actual) {
+ assert(expected.hasOwnProperty(p), "assert_object_equals", description,
+ "unexpected property ${p}", {p:p});
+
+ if (typeof actual[p] === "object" && actual[p] !== null) {
+ if (stack.indexOf(actual[p]) === -1) {
+ check_equal(actual[p], expected[p], stack);
+ }
+ } else {
+ assert(same_value(actual[p], expected[p]), "assert_object_equals", description,
+ "property ${p} expected ${expected} got ${actual}",
+ {p:p, expected:expected[p], actual:actual[p]});
+ }
+ }
+ for (p in expected) {
+ assert(actual.hasOwnProperty(p),
+ "assert_object_equals", description,
+ "expected property ${p} missing", {p:p});
+ }
+ stack.pop();
+ }
+ check_equal(actual, expected, []);
+ }
+ expose_assert(assert_object_equals, "assert_object_equals");
+
+ /**
+ * Assert that ``actual`` and ``expected`` are both arrays, and that the array properties of
+ * ``actual`` and ``expected`` are all the same value (as for :js:func:`assert_equals`).
+ *
+ * @param {Array} actual - Test array.
+ * @param {Array} expected - Array that is expected to contain the same values as ``actual``.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_array_equals(actual, expected, description)
+ {
+ const max_array_length = 20;
+ function shorten_array(arr, offset = 0) {
+ // Make ", …" only show up when it would likely reduce the length, not accounting for
+ // fonts.
+ if (arr.length < max_array_length + 2) {
+ return arr;
+ }
+ // By default we want half the elements after the offset and half before
+ // But if that takes us past the end of the array, we have more before, and
+ // if it takes us before the start we have more after.
+ const length_after_offset = Math.floor(max_array_length / 2);
+ let upper_bound = Math.min(length_after_offset + offset, arr.length);
+ const lower_bound = Math.max(upper_bound - max_array_length, 0);
+
+ if (lower_bound === 0) {
+ upper_bound = max_array_length;
+ }
+
+ const output = arr.slice(lower_bound, upper_bound);
+ if (lower_bound > 0) {
+ output.beginEllipsis = true;
+ }
+ if (upper_bound < arr.length) {
+ output.endEllipsis = true;
+ }
+ return output;
+ }
+
+ assert(typeof actual === "object" && actual !== null && "length" in actual,
+ "assert_array_equals", description,
+ "value is ${actual}, expected array",
+ {actual:actual});
+ assert(actual.length === expected.length,
+ "assert_array_equals", description,
+ "lengths differ, expected array ${expected} length ${expectedLength}, got ${actual} length ${actualLength}",
+ {expected:shorten_array(expected, expected.length - 1), expectedLength:expected.length,
+ actual:shorten_array(actual, actual.length - 1), actualLength:actual.length
+ });
+
+ for (var i = 0; i < actual.length; i++) {
+ assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i),
+ "assert_array_equals", description,
+ "expected property ${i} to be ${expected} but was ${actual} (expected array ${arrayExpected} got ${arrayActual})",
+ {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing",
+ actual:actual.hasOwnProperty(i) ? "present" : "missing",
+ arrayExpected:shorten_array(expected, i), arrayActual:shorten_array(actual, i)});
+ assert(same_value(expected[i], actual[i]),
+ "assert_array_equals", description,
+ "expected property ${i} to be ${expected} but got ${actual} (expected array ${arrayExpected} got ${arrayActual})",
+ {i:i, expected:expected[i], actual:actual[i],
+ arrayExpected:shorten_array(expected, i), arrayActual:shorten_array(actual, i)});
+ }
+ }
+ expose_assert(assert_array_equals, "assert_array_equals");
+
+ /**
+ * Assert that each array property in ``actual`` is a number within
+ * ± `epsilon` of the corresponding property in `expected`.
+ *
+ * @param {Array} actual - Array of test values.
+ * @param {Array} expected - Array of values expected to be close to the values in ``actual``.
+ * @param {number} epsilon - Magnitude of allowed difference
+ * between each value in ``actual`` and ``expected``.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_array_approx_equals(actual, expected, epsilon, description)
+ {
+ /*
+ * Test if two primitive arrays are equal within +/- epsilon
+ */
+ assert(actual.length === expected.length,
+ "assert_array_approx_equals", description,
+ "lengths differ, expected ${expected} got ${actual}",
+ {expected:expected.length, actual:actual.length});
+
+ for (var i = 0; i < actual.length; i++) {
+ assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i),
+ "assert_array_approx_equals", description,
+ "property ${i}, property expected to be ${expected} but was ${actual}",
+ {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing",
+ actual:actual.hasOwnProperty(i) ? "present" : "missing"});
+ assert(typeof actual[i] === "number",
+ "assert_array_approx_equals", description,
+ "property ${i}, expected a number but got a ${type_actual}",
+ {i:i, type_actual:typeof actual[i]});
+ assert(Math.abs(actual[i] - expected[i]) <= epsilon,
+ "assert_array_approx_equals", description,
+ "property ${i}, expected ${expected} +/- ${epsilon}, expected ${expected} but got ${actual}",
+ {i:i, expected:expected[i], actual:actual[i], epsilon:epsilon});
+ }
+ }
+ expose_assert(assert_array_approx_equals, "assert_array_approx_equals");
+
+ /**
+ * Assert that ``actual`` is within ± ``epsilon`` of ``expected``.
+ *
+ * @param {number} actual - Test value.
+ * @param {number} expected - Value number is expected to be close to.
+ * @param {number} epsilon - Magnitude of allowed difference between ``actual`` and ``expected``.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_approx_equals(actual, expected, epsilon, description)
+ {
+ /*
+ * Test if two primitive numbers are equal within +/- epsilon
+ */
+ assert(typeof actual === "number",
+ "assert_approx_equals", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ // The epsilon math below does not place nice with NaN and Infinity
+ // But in this case Infinity = Infinity and NaN = NaN
+ if (isFinite(actual) || isFinite(expected)) {
+ assert(Math.abs(actual - expected) <= epsilon,
+ "assert_approx_equals", description,
+ "expected ${expected} +/- ${epsilon} but got ${actual}",
+ {expected:expected, actual:actual, epsilon:epsilon});
+ } else {
+ assert_equals(actual, expected);
+ }
+ }
+ expose_assert(assert_approx_equals, "assert_approx_equals");
+
+ /**
+ * Assert that ``actual`` is a number less than ``expected``.
+ *
+ * @param {number} actual - Test value.
+ * @param {number} expected - Number that ``actual`` must be less than.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_less_than(actual, expected, description)
+ {
+ /*
+ * Test if a primitive number is less than another
+ */
+ assert(typeof actual === "number",
+ "assert_less_than", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ assert(actual < expected,
+ "assert_less_than", description,
+ "expected a number less than ${expected} but got ${actual}",
+ {expected:expected, actual:actual});
+ }
+ expose_assert(assert_less_than, "assert_less_than");
+
+ /**
+ * Assert that ``actual`` is a number greater than ``expected``.
+ *
+ * @param {number} actual - Test value.
+ * @param {number} expected - Number that ``actual`` must be greater than.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_greater_than(actual, expected, description)
+ {
+ /*
+ * Test if a primitive number is greater than another
+ */
+ assert(typeof actual === "number",
+ "assert_greater_than", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ assert(actual > expected,
+ "assert_greater_than", description,
+ "expected a number greater than ${expected} but got ${actual}",
+ {expected:expected, actual:actual});
+ }
+ expose_assert(assert_greater_than, "assert_greater_than");
+
+ /**
+ * Assert that ``actual`` is a number greater than ``lower`` and less
+ * than ``upper`` but not equal to either.
+ *
+ * @param {number} actual - Test value.
+ * @param {number} lower - Number that ``actual`` must be greater than.
+ * @param {number} upper - Number that ``actual`` must be less than.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_between_exclusive(actual, lower, upper, description)
+ {
+ /*
+ * Test if a primitive number is between two others
+ */
+ assert(typeof actual === "number",
+ "assert_between_exclusive", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ assert(actual > lower && actual < upper,
+ "assert_between_exclusive", description,
+ "expected a number greater than ${lower} " +
+ "and less than ${upper} but got ${actual}",
+ {lower:lower, upper:upper, actual:actual});
+ }
+ expose_assert(assert_between_exclusive, "assert_between_exclusive");
+
+ /**
+ * Assert that ``actual`` is a number less than or equal to ``expected``.
+ *
+ * @param {number} actual - Test value.
+ * @param {number} expected - Number that ``actual`` must be less
+ * than or equal to.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_less_than_equal(actual, expected, description)
+ {
+ /*
+ * Test if a primitive number is less than or equal to another
+ */
+ assert(typeof actual === "number",
+ "assert_less_than_equal", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ assert(actual <= expected,
+ "assert_less_than_equal", description,
+ "expected a number less than or equal to ${expected} but got ${actual}",
+ {expected:expected, actual:actual});
+ }
+ expose_assert(assert_less_than_equal, "assert_less_than_equal");
+
+ /**
+ * Assert that ``actual`` is a number greater than or equal to ``expected``.
+ *
+ * @param {number} actual - Test value.
+ * @param {number} expected - Number that ``actual`` must be greater
+ * than or equal to.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_greater_than_equal(actual, expected, description)
+ {
+ /*
+ * Test if a primitive number is greater than or equal to another
+ */
+ assert(typeof actual === "number",
+ "assert_greater_than_equal", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ assert(actual >= expected,
+ "assert_greater_than_equal", description,
+ "expected a number greater than or equal to ${expected} but got ${actual}",
+ {expected:expected, actual:actual});
+ }
+ expose_assert(assert_greater_than_equal, "assert_greater_than_equal");
+
+ /**
+ * Assert that ``actual`` is a number greater than or equal to ``lower`` and less
+ * than or equal to ``upper``.
+ *
+ * @param {number} actual - Test value.
+ * @param {number} lower - Number that ``actual`` must be greater than or equal to.
+ * @param {number} upper - Number that ``actual`` must be less than or equal to.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_between_inclusive(actual, lower, upper, description)
+ {
+ /*
+ * Test if a primitive number is between to two others or equal to either of them
+ */
+ assert(typeof actual === "number",
+ "assert_between_inclusive", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ assert(actual >= lower && actual <= upper,
+ "assert_between_inclusive", description,
+ "expected a number greater than or equal to ${lower} " +
+ "and less than or equal to ${upper} but got ${actual}",
+ {lower:lower, upper:upper, actual:actual});
+ }
+ expose_assert(assert_between_inclusive, "assert_between_inclusive");
+
+ /**
+ * Assert that ``actual`` matches the RegExp ``expected``.
+ *
+ * @param {String} actual - Test string.
+ * @param {RegExp} expected - RegExp ``actual`` must match.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_regexp_match(actual, expected, description) {
+ /*
+ * Test if a string (actual) matches a regexp (expected)
+ */
+ assert(expected.test(actual),
+ "assert_regexp_match", description,
+ "expected ${expected} but got ${actual}",
+ {expected:expected, actual:actual});
+ }
+ expose_assert(assert_regexp_match, "assert_regexp_match");
+
+ /**
+ * Assert that the class string of ``object`` as returned in
+ * ``Object.prototype.toString`` is equal to ``class_name``.
+ *
+ * @param {Object} object - Object to stringify.
+ * @param {string} class_string - Expected class string for ``object``.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_class_string(object, class_string, description) {
+ var actual = {}.toString.call(object);
+ var expected = "[object " + class_string + "]";
+ assert(same_value(actual, expected), "assert_class_string", description,
+ "expected ${expected} but got ${actual}",
+ {expected:expected, actual:actual});
+ }
+ expose_assert(assert_class_string, "assert_class_string");
+
+ /**
+ * Assert that ``object`` has an own property with name ``property_name``.
+ *
+ * @param {Object} object - Object that should have the given property.
+ * @param {string} property_name - Expected property name.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_own_property(object, property_name, description) {
+ assert(object.hasOwnProperty(property_name),
+ "assert_own_property", description,
+ "expected property ${p} missing", {p:property_name});
+ }
+ expose_assert(assert_own_property, "assert_own_property");
+
+ /**
+ * Assert that ``object`` does not have an own property with name ``property_name``.
+ *
+ * @param {Object} object - Object that should not have the given property.
+ * @param {string} property_name - Property name to test.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_not_own_property(object, property_name, description) {
+ assert(!object.hasOwnProperty(property_name),
+ "assert_not_own_property", description,
+ "unexpected property ${p} is found on object", {p:property_name});
+ }
+ expose_assert(assert_not_own_property, "assert_not_own_property");
+
+ function _assert_inherits(name) {
+ return function (object, property_name, description)
+ {
+ assert((typeof object === "object" && object !== null) ||
+ typeof object === "function" ||
+ // Or has [[IsHTMLDDA]] slot
+ String(object) === "[object HTMLAllCollection]",
+ name, description,
+ "provided value is not an object");
+
+ assert("hasOwnProperty" in object,
+ name, description,
+ "provided value is an object but has no hasOwnProperty method");
+
+ assert(!object.hasOwnProperty(property_name),
+ name, description,
+ "property ${p} found on object expected in prototype chain",
+ {p:property_name});
+
+ assert(property_name in object,
+ name, description,
+ "property ${p} not found in prototype chain",
+ {p:property_name});
+ };
+ }
+
+ /**
+ * Assert that ``object`` does not have an own property with name
+ * ``property_name``, but inherits one through the prototype chain.
+ *
+ * @param {Object} object - Object that should have the given property in its prototype chain.
+ * @param {string} property_name - Expected property name.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_inherits(object, property_name, description) {
+ return _assert_inherits("assert_inherits")(object, property_name, description);
+ }
+ expose_assert(assert_inherits, "assert_inherits");
+
+ /**
+ * Alias for :js:func:`insert_inherits`.
+ *
+ * @param {Object} object - Object that should have the given property in its prototype chain.
+ * @param {string} property_name - Expected property name.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_idl_attribute(object, property_name, description) {
+ return _assert_inherits("assert_idl_attribute")(object, property_name, description);
+ }
+ expose_assert(assert_idl_attribute, "assert_idl_attribute");
+
+
+ /**
+ * Assert that ``object`` has a property named ``property_name`` and that the property is readonly.
+ *
+ * Note: The implementation tries to update the named property, so
+ * any side effects of updating will be triggered. Users are
+ * encouraged to instead inspect the property descriptor of ``property_name`` on ``object``.
+ *
+ * @param {Object} object - Object that should have the given property in its prototype chain.
+ * @param {string} property_name - Expected property name.
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_readonly(object, property_name, description)
+ {
+ var initial_value = object[property_name];
+ try {
+ //Note that this can have side effects in the case where
+ //the property has PutForwards
+ object[property_name] = initial_value + "a"; //XXX use some other value here?
+ assert(same_value(object[property_name], initial_value),
+ "assert_readonly", description,
+ "changing property ${p} succeeded",
+ {p:property_name});
+ } finally {
+ object[property_name] = initial_value;
+ }
+ }
+ expose_assert(assert_readonly, "assert_readonly");
+
+ /**
+ * Assert a JS Error with the expected constructor is thrown.
+ *
+ * @param {object} constructor The expected exception constructor.
+ * @param {Function} func Function which should throw.
+ * @param {string} [description] Error description for the case that the error is not thrown.
+ */
+ function assert_throws_js(constructor, func, description)
+ {
+ assert_throws_js_impl(constructor, func, description,
+ "assert_throws_js");
+ }
+ expose_assert(assert_throws_js, "assert_throws_js");
+
+ /**
+ * Like assert_throws_js but allows specifying the assertion type
+ * (assert_throws_js or promise_rejects_js, in practice).
+ */
+ function assert_throws_js_impl(constructor, func, description,
+ assertion_type)
+ {
+ try {
+ func.call(this);
+ assert(false, assertion_type, description,
+ "${func} did not throw", {func:func});
+ } catch (e) {
+ if (e instanceof AssertionError) {
+ throw e;
+ }
+
+ // Basic sanity-checks on the thrown exception.
+ assert(typeof e === "object",
+ assertion_type, description,
+ "${func} threw ${e} with type ${type}, not an object",
+ {func:func, e:e, type:typeof e});
+
+ assert(e !== null,
+ assertion_type, description,
+ "${func} threw null, not an object",
+ {func:func});
+
+ // Basic sanity-check on the passed-in constructor
+ assert(typeof constructor == "function",
+ assertion_type, description,
+ "${constructor} is not a constructor",
+ {constructor:constructor});
+ var obj = constructor;
+ while (obj) {
+ if (typeof obj === "function" &&
+ obj.name === "Error") {
+ break;
+ }
+ obj = Object.getPrototypeOf(obj);
+ }
+ assert(obj != null,
+ assertion_type, description,
+ "${constructor} is not an Error subtype",
+ {constructor:constructor});
+
+ // And checking that our exception is reasonable
+ assert(e.constructor === constructor &&
+ e.name === constructor.name,
+ assertion_type, description,
+ "${func} threw ${actual} (${actual_name}) expected instance of ${expected} (${expected_name})",
+ {func:func, actual:e, actual_name:e.name,
+ expected:constructor,
+ expected_name:constructor.name});
+ }
+ }
+
+ // TODO: Figure out how to document the overloads better.
+ // sphinx-js doesn't seem to handle @variation correctly,
+ // and only expects a single JSDoc entry per function.
+ /**
+ * Assert a DOMException with the expected type is thrown.
+ *
+ * There are two ways of calling assert_throws_dom:
+ *
+ * 1) If the DOMException is expected to come from the current global, the
+ * second argument should be the function expected to throw and a third,
+ * optional, argument is the assertion description.
+ *
+ * 2) If the DOMException is expected to come from some other global, the
+ * second argument should be the DOMException constructor from that global,
+ * the third argument the function expected to throw, and the fourth, optional,
+ * argument the assertion description.
+ *
+ * @param {number|string} type - The expected exception name or
+ * code. See the `table of names and codes
+ * <https://webidl.spec.whatwg.org/#dfn-error-names-table>`_. If a
+ * number is passed it should be one of the numeric code values in
+ * that table (e.g. 3, 4, etc). If a string is passed it can
+ * either be an exception name (e.g. "HierarchyRequestError",
+ * "WrongDocumentError") or the name of the corresponding error
+ * code (e.g. "``HIERARCHY_REQUEST_ERR``", "``WRONG_DOCUMENT_ERR``").
+ * @param {Function} descriptionOrFunc - The function expected to
+ * throw (if the exception comes from another global), or the
+ * optional description of the condition being tested (if the
+ * exception comes from the current global).
+ * @param {string} [description] - Description of the condition
+ * being tested (if the exception comes from another global).
+ *
+ */
+ function assert_throws_dom(type, funcOrConstructor, descriptionOrFunc, maybeDescription)
+ {
+ let constructor, func, description;
+ if (funcOrConstructor.name === "DOMException") {
+ constructor = funcOrConstructor;
+ func = descriptionOrFunc;
+ description = maybeDescription;
+ } else {
+ constructor = self.DOMException;
+ func = funcOrConstructor;
+ description = descriptionOrFunc;
+ assert(maybeDescription === undefined,
+ "Too many args pased to no-constructor version of assert_throws_dom");
+ }
+ assert_throws_dom_impl(type, func, description, "assert_throws_dom", constructor)
+ }
+ expose_assert(assert_throws_dom, "assert_throws_dom");
+
+ /**
+ * Similar to assert_throws_dom but allows specifying the assertion type
+ * (assert_throws_dom or promise_rejects_dom, in practice). The
+ * "constructor" argument must be the DOMException constructor from the
+ * global we expect the exception to come from.
+ */
+ function assert_throws_dom_impl(type, func, description, assertion_type, constructor)
+ {
+ try {
+ func.call(this);
+ assert(false, assertion_type, description,
+ "${func} did not throw", {func:func});
+ } catch (e) {
+ if (e instanceof AssertionError) {
+ throw e;
+ }
+
+ // Basic sanity-checks on the thrown exception.
+ assert(typeof e === "object",
+ assertion_type, description,
+ "${func} threw ${e} with type ${type}, not an object",
+ {func:func, e:e, type:typeof e});
+
+ assert(e !== null,
+ assertion_type, description,
+ "${func} threw null, not an object",
+ {func:func});
+
+ // Sanity-check our type
+ assert(typeof type == "number" ||
+ typeof type == "string",
+ assertion_type, description,
+ "${type} is not a number or string",
+ {type:type});
+
+ var codename_name_map = {
+ INDEX_SIZE_ERR: 'IndexSizeError',
+ HIERARCHY_REQUEST_ERR: 'HierarchyRequestError',
+ WRONG_DOCUMENT_ERR: 'WrongDocumentError',
+ INVALID_CHARACTER_ERR: 'InvalidCharacterError',
+ NO_MODIFICATION_ALLOWED_ERR: 'NoModificationAllowedError',
+ NOT_FOUND_ERR: 'NotFoundError',
+ NOT_SUPPORTED_ERR: 'NotSupportedError',
+ INUSE_ATTRIBUTE_ERR: 'InUseAttributeError',
+ INVALID_STATE_ERR: 'InvalidStateError',
+ SYNTAX_ERR: 'SyntaxError',
+ INVALID_MODIFICATION_ERR: 'InvalidModificationError',
+ NAMESPACE_ERR: 'NamespaceError',
+ INVALID_ACCESS_ERR: 'InvalidAccessError',
+ TYPE_MISMATCH_ERR: 'TypeMismatchError',
+ SECURITY_ERR: 'SecurityError',
+ NETWORK_ERR: 'NetworkError',
+ ABORT_ERR: 'AbortError',
+ URL_MISMATCH_ERR: 'URLMismatchError',
+ QUOTA_EXCEEDED_ERR: 'QuotaExceededError',
+ TIMEOUT_ERR: 'TimeoutError',
+ INVALID_NODE_TYPE_ERR: 'InvalidNodeTypeError',
+ DATA_CLONE_ERR: 'DataCloneError'
+ };
+
+ var name_code_map = {
+ IndexSizeError: 1,
+ HierarchyRequestError: 3,
+ WrongDocumentError: 4,
+ InvalidCharacterError: 5,
+ NoModificationAllowedError: 7,
+ NotFoundError: 8,
+ NotSupportedError: 9,
+ InUseAttributeError: 10,
+ InvalidStateError: 11,
+ SyntaxError: 12,
+ InvalidModificationError: 13,
+ NamespaceError: 14,
+ InvalidAccessError: 15,
+ TypeMismatchError: 17,
+ SecurityError: 18,
+ NetworkError: 19,
+ AbortError: 20,
+ URLMismatchError: 21,
+ QuotaExceededError: 22,
+ TimeoutError: 23,
+ InvalidNodeTypeError: 24,
+ DataCloneError: 25,
+
+ EncodingError: 0,
+ NotReadableError: 0,
+ UnknownError: 0,
+ ConstraintError: 0,
+ DataError: 0,
+ TransactionInactiveError: 0,
+ ReadOnlyError: 0,
+ VersionError: 0,
+ OperationError: 0,
+ NotAllowedError: 0,
+ OptOutError: 0
+ };
+
+ var code_name_map = {};
+ for (var key in name_code_map) {
+ if (name_code_map[key] > 0) {
+ code_name_map[name_code_map[key]] = key;
+ }
+ }
+
+ var required_props = {};
+ var name;
+
+ if (typeof type === "number") {
+ if (type === 0) {
+ throw new AssertionError('Test bug: ambiguous DOMException code 0 passed to assert_throws_dom()');
+ } else if (!(type in code_name_map)) {
+ throw new AssertionError('Test bug: unrecognized DOMException code "' + type + '" passed to assert_throws_dom()');
+ }
+ name = code_name_map[type];
+ required_props.code = type;
+ } else if (typeof type === "string") {
+ name = type in codename_name_map ? codename_name_map[type] : type;
+ if (!(name in name_code_map)) {
+ throw new AssertionError('Test bug: unrecognized DOMException code name or name "' + type + '" passed to assert_throws_dom()');
+ }
+
+ required_props.code = name_code_map[name];
+ }
+
+ if (required_props.code === 0 ||
+ ("name" in e &&
+ e.name !== e.name.toUpperCase() &&
+ e.name !== "DOMException")) {
+ // New style exception: also test the name property.
+ required_props.name = name;
+ }
+
+ for (var prop in required_props) {
+ assert(prop in e && e[prop] == required_props[prop],
+ assertion_type, description,
+ "${func} threw ${e} that is not a DOMException " + type + ": property ${prop} is equal to ${actual}, expected ${expected}",
+ {func:func, e:e, prop:prop, actual:e[prop], expected:required_props[prop]});
+ }
+
+ // Check that the exception is from the right global. This check is last
+ // so more specific, and more informative, checks on the properties can
+ // happen in case a totally incorrect exception is thrown.
+ assert(e.constructor === constructor,
+ assertion_type, description,
+ "${func} threw an exception from the wrong global",
+ {func});
+
+ }
+ }
+
+ /**
+ * Assert the provided value is thrown.
+ *
+ * @param {value} exception The expected exception.
+ * @param {Function} func Function which should throw.
+ * @param {string} [description] Error description for the case that the error is not thrown.
+ */
+ function assert_throws_exactly(exception, func, description)
+ {
+ assert_throws_exactly_impl(exception, func, description,
+ "assert_throws_exactly");
+ }
+ expose_assert(assert_throws_exactly, "assert_throws_exactly");
+
+ /**
+ * Like assert_throws_exactly but allows specifying the assertion type
+ * (assert_throws_exactly or promise_rejects_exactly, in practice).
+ */
+ function assert_throws_exactly_impl(exception, func, description,
+ assertion_type)
+ {
+ try {
+ func.call(this);
+ assert(false, assertion_type, description,
+ "${func} did not throw", {func:func});
+ } catch (e) {
+ if (e instanceof AssertionError) {
+ throw e;
+ }
+
+ assert(same_value(e, exception), assertion_type, description,
+ "${func} threw ${e} but we expected it to throw ${exception}",
+ {func:func, e:e, exception:exception});
+ }
+ }
+
+ /**
+ * Asserts if called. Used to ensure that a specific codepath is
+ * not taken e.g. that an error event isn't fired.
+ *
+ * @param {string} [description] - Description of the condition being tested.
+ */
+ function assert_unreached(description) {
+ assert(false, "assert_unreached", description,
+ "Reached unreachable code");
+ }
+ expose_assert(assert_unreached, "assert_unreached");
+
+ /**
+ * @callback AssertFunc
+ * @param {Any} actual
+ * @param {Any} expected
+ * @param {Any[]} args
+ */
+
+ /**
+ * Asserts that ``actual`` matches at least one value of ``expected``
+ * according to a comparison defined by ``assert_func``.
+ *
+ * Note that tests with multiple allowed pass conditions are bad
+ * practice unless the spec specifically allows multiple
+ * behaviours. Test authors should not use this method simply to
+ * hide UA bugs.
+ *
+ * @param {AssertFunc} assert_func - Function to compare actual
+ * and expected. It must throw when the comparison fails and
+ * return when the comparison passes.
+ * @param {Any} actual - Test value.
+ * @param {Array} expected_array - Array of possible expected values.
+ * @param {Any[]} args - Additional arguments to pass to ``assert_func``.
+ */
+ function assert_any(assert_func, actual, expected_array, ...args)
+ {
+ var errors = [];
+ var passed = false;
+ forEach(expected_array,
+ function(expected)
+ {
+ try {
+ assert_func.apply(this, [actual, expected].concat(args));
+ passed = true;
+ } catch (e) {
+ errors.push(e.message);
+ }
+ });
+ if (!passed) {
+ throw new AssertionError(errors.join("\n\n"));
+ }
+ }
+ // FIXME: assert_any cannot use expose_assert, because assert_wrapper does
+ // not support nested assert calls (e.g. to assert_func). We need to
+ // support bypassing assert_wrapper for the inner asserts here.
+ expose(assert_any, "assert_any");
+
+ /**
+ * Assert that a feature is implemented, based on a 'truthy' condition.
+ *
+ * This function should be used to early-exit from tests in which there is
+ * no point continuing without support for a non-optional spec or spec
+ * feature. For example:
+ *
+ * assert_implements(window.Foo, 'Foo is not supported');
+ *
+ * @param {object} condition The truthy value to test
+ * @param {string} [description] Error description for the case that the condition is not truthy.
+ */
+ function assert_implements(condition, description) {
+ assert(!!condition, "assert_implements", description);
+ }
+ expose_assert(assert_implements, "assert_implements")
+
+ /**
+ * Assert that an optional feature is implemented, based on a 'truthy' condition.
+ *
+ * This function should be used to early-exit from tests in which there is
+ * no point continuing without support for an explicitly optional spec or
+ * spec feature. For example:
+ *
+ * assert_implements_optional(video.canPlayType("video/webm"),
+ * "webm video playback not supported");
+ *
+ * @param {object} condition The truthy value to test
+ * @param {string} [description] Error description for the case that the condition is not truthy.
+ */
+ function assert_implements_optional(condition, description) {
+ if (!condition) {
+ throw new OptionalFeatureUnsupportedError(description);
+ }
+ }
+ expose_assert(assert_implements_optional, "assert_implements_optional");
+
+ /**
+ * @class
+ *
+ * A single subtest. A Test is not constructed directly but via the
+ * :js:func:`test`, :js:func:`async_test` or :js:func:`promise_test` functions.
+ *
+ * @param {string} name - This must be unique in a given file and must be
+ * invariant between runs.
+ *
+ */
+ function Test(name, properties)
+ {
+ if (tests.file_is_test && tests.tests.length) {
+ throw new Error("Tried to create a test with file_is_test");
+ }
+ /** The test name. */
+ this.name = name;
+
+ this.phase = (tests.is_aborted || tests.phase === tests.phases.COMPLETE) ?
+ this.phases.COMPLETE : this.phases.INITIAL;
+
+ /** The test status code.*/
+ this.status = this.NOTRUN;
+ this.timeout_id = null;
+ this.index = null;
+
+ this.properties = properties || {};
+ this.timeout_length = settings.test_timeout;
+ if (this.timeout_length !== null) {
+ this.timeout_length *= tests.timeout_multiplier;
+ }
+
+ /** A message indicating the reason for test failure. */
+ this.message = null;
+ /** Stack trace in case of failure. */
+ this.stack = null;
+
+ this.steps = [];
+ this._is_promise_test = false;
+
+ this.cleanup_callbacks = [];
+ this._user_defined_cleanup_count = 0;
+ this._done_callbacks = [];
+
+ if (typeof AbortController === "function") {
+ this._abortController = new AbortController();
+ }
+
+ // Tests declared following harness completion are likely an indication
+ // of a programming error, but they cannot be reported
+ // deterministically.
+ if (tests.phase === tests.phases.COMPLETE) {
+ return;
+ }
+
+ tests.push(this);
+ }
+
+ /**
+ * Enum of possible test statuses.
+ *
+ * :values:
+ * - ``PASS``
+ * - ``FAIL``
+ * - ``TIMEOUT``
+ * - ``NOTRUN``
+ * - ``PRECONDITION_FAILED``
+ */
+ Test.statuses = {
+ PASS:0,
+ FAIL:1,
+ TIMEOUT:2,
+ NOTRUN:3,
+ PRECONDITION_FAILED:4
+ };
+
+ Test.prototype = merge({}, Test.statuses);
+
+ Test.prototype.phases = {
+ INITIAL:0,
+ STARTED:1,
+ HAS_RESULT:2,
+ CLEANING:3,
+ COMPLETE:4
+ };
+
+ Test.prototype.status_formats = {
+ 0: "Pass",
+ 1: "Fail",
+ 2: "Timeout",
+ 3: "Not Run",
+ 4: "Optional Feature Unsupported",
+ }
+
+ Test.prototype.format_status = function() {
+ return this.status_formats[this.status];
+ }
+
+ Test.prototype.structured_clone = function()
+ {
+ if (!this._structured_clone) {
+ var msg = this.message;
+ msg = msg ? String(msg) : msg;
+ this._structured_clone = merge({
+ name:String(this.name),
+ properties:merge({}, this.properties),
+ phases:merge({}, this.phases)
+ }, Test.statuses);
+ }
+ this._structured_clone.status = this.status;
+ this._structured_clone.message = this.message;
+ this._structured_clone.stack = this.stack;
+ this._structured_clone.index = this.index;
+ this._structured_clone.phase = this.phase;
+ return this._structured_clone;
+ };
+
+ /**
+ * Run a single step of an ongoing test.
+ *
+ * @param {string} func - Callback function to run as a step. If
+ * this throws an :js:func:`AssertionError`, or any other
+ * exception, the :js:class:`Test` status is set to ``FAIL``.
+ * @param {Object} [this_obj] - The object to use as the this
+ * value when calling ``func``. Defaults to the :js:class:`Test` object.
+ */
+ Test.prototype.step = function(func, this_obj)
+ {
+ if (this.phase > this.phases.STARTED) {
+ return;
+ }
+
+ if (settings.debug && this.phase !== this.phases.STARTED) {
+ console.log("TEST START", this.name);
+ }
+ this.phase = this.phases.STARTED;
+ //If we don't get a result before the harness times out that will be a test timeout
+ this.set_status(this.TIMEOUT, "Test timed out");
+
+ tests.started = true;
+ tests.current_test = this;
+ tests.notify_test_state(this);
+
+ if (this.timeout_id === null) {
+ this.set_timeout();
+ }
+
+ this.steps.push(func);
+
+ if (arguments.length === 1) {
+ this_obj = this;
+ }
+
+ if (settings.debug) {
+ console.debug("TEST STEP", this.name);
+ }
+
+ try {
+ return func.apply(this_obj, Array.prototype.slice.call(arguments, 2));
+ } catch (e) {
+ if (this.phase >= this.phases.HAS_RESULT) {
+ return;
+ }
+ var status = e instanceof OptionalFeatureUnsupportedError ? this.PRECONDITION_FAILED : this.FAIL;
+ var message = String((typeof e === "object" && e !== null) ? e.message : e);
+ var stack = e.stack ? e.stack : null;
+
+ this.set_status(status, message, stack);
+ this.phase = this.phases.HAS_RESULT;
+ this.done();
+ } finally {
+ this.current_test = null;
+ }
+ };
+
+ /**
+ * Wrap a function so that it runs as a step of the current test.
+ *
+ * This allows creating a callback function that will run as a
+ * test step.
+ *
+ * @example
+ * let t = async_test("Example");
+ * onload = t.step_func(e => {
+ * assert_equals(e.name, "load");
+ * // Mark the test as complete.
+ * t.done();
+ * })
+ *
+ * @param {string} func - Function to run as a step. If this
+ * throws an :js:func:`AssertionError`, or any other exception,
+ * the :js:class:`Test` status is set to ``FAIL``.
+ * @param {Object} [this_obj] - The object to use as the this
+ * value when calling ``func``. Defaults to the :js:class:`Test` object.
+ */
+ Test.prototype.step_func = function(func, this_obj)
+ {
+ var test_this = this;
+
+ if (arguments.length === 1) {
+ this_obj = test_this;
+ }
+
+ return function()
+ {
+ return test_this.step.apply(test_this, [func, this_obj].concat(
+ Array.prototype.slice.call(arguments)));
+ };
+ };
+
+ /**
+ * Wrap a function so that it runs as a step of the current test,
+ * and automatically marks the test as complete if the function
+ * returns without error.
+ *
+ * @param {string} func - Function to run as a step. If this
+ * throws an :js:func:`AssertionError`, or any other exception,
+ * the :js:class:`Test` status is set to ``FAIL``. If it returns
+ * without error the status is set to ``PASS``.
+ * @param {Object} [this_obj] - The object to use as the this
+ * value when calling `func`. Defaults to the :js:class:`Test` object.
+ */
+ Test.prototype.step_func_done = function(func, this_obj)
+ {
+ var test_this = this;
+
+ if (arguments.length === 1) {
+ this_obj = test_this;
+ }
+
+ return function()
+ {
+ if (func) {
+ test_this.step.apply(test_this, [func, this_obj].concat(
+ Array.prototype.slice.call(arguments)));
+ }
+ test_this.done();
+ };
+ };
+
+ /**
+ * Return a function that automatically sets the current test to
+ * ``FAIL`` if it's called.
+ *
+ * @param {string} [description] - Error message to add to assert
+ * in case of failure.
+ *
+ */
+ Test.prototype.unreached_func = function(description)
+ {
+ return this.step_func(function() {
+ assert_unreached(description);
+ });
+ };
+
+ /**
+ * Run a function as a step of the test after a given timeout.
+ *
+ * This multiplies the timeout by the global timeout multiplier to
+ * account for the expected execution speed of the current test
+ * environment. For example ``test.step_timeout(f, 2000)`` with a
+ * timeout multiplier of 2 will wait for 4000ms before calling ``f``.
+ *
+ * In general it's encouraged to use :js:func:`Test.step_wait` or
+ * :js:func:`step_wait_func` in preference to this function where possible,
+ * as they provide better test performance.
+ *
+ * @param {Function} func - Function to run as a test
+ * step.
+ * @param {number} timeout - Time in ms to wait before running the
+ * test step. The actual wait time is ``timeout`` x
+ * ``timeout_multiplier``.
+ *
+ */
+ Test.prototype.step_timeout = function(func, timeout) {
+ var test_this = this;
+ var args = Array.prototype.slice.call(arguments, 2);
+ return setTimeout(this.step_func(function() {
+ return func.apply(test_this, args);
+ }), timeout * tests.timeout_multiplier);
+ };
+
+ /**
+ * Poll for a function to return true, and call a callback
+ * function once it does, or assert if a timeout is
+ * reached. This is preferred over a simple step_timeout
+ * whenever possible since it allows the timeout to be longer
+ * to reduce intermittents without compromising test execution
+ * speed when the condition is quickly met.
+ *
+ * @param {Function} cond A function taking no arguments and
+ * returning a boolean. The callback is called
+ * when this function returns true.
+ * @param {Function} func A function taking no arguments to call once
+ * the condition is met.
+ * @param {string} [description] Error message to add to assert in case of
+ * failure.
+ * @param {number} timeout Timeout in ms. This is multiplied by the global
+ * timeout_multiplier
+ * @param {number} interval Polling interval in ms
+ *
+ */
+ Test.prototype.step_wait_func = function(cond, func, description,
+ timeout=3000, interval=100) {
+ var timeout_full = timeout * tests.timeout_multiplier;
+ var remaining = Math.ceil(timeout_full / interval);
+ var test_this = this;
+
+ var wait_for_inner = test_this.step_func(() => {
+ if (cond()) {
+ func();
+ } else {
+ if(remaining === 0) {
+ assert(false, "step_wait_func", description,
+ "Timed out waiting on condition");
+ }
+ remaining--;
+ setTimeout(wait_for_inner, interval);
+ }
+ });
+
+ wait_for_inner();
+ };
+
+ /**
+ * Poll for a function to return true, and invoke a callback
+ * followed by this.done() once it does, or assert if a timeout
+ * is reached. This is preferred over a simple step_timeout
+ * whenever possible since it allows the timeout to be longer
+ * to reduce intermittents without compromising test execution speed
+ * when the condition is quickly met.
+ *
+ * @example
+ * async_test(t => {
+ * const popup = window.open("resources/coop-coep.py?coop=same-origin&coep=&navigate=about:blank");
+ * t.add_cleanup(() => popup.close());
+ * assert_equals(window, popup.opener);
+ *
+ * popup.onload = t.step_func(() => {
+ * assert_true(popup.location.href.endsWith("&navigate=about:blank"));
+ * // Use step_wait_func_done as about:blank cannot message back.
+ * t.step_wait_func_done(() => popup.location.href === "about:blank");
+ * });
+ * }, "Navigating a popup to about:blank");
+ *
+ * @param {Function} cond A function taking no arguments and
+ * returning a boolean. The callback is called
+ * when this function returns true.
+ * @param {Function} func A function taking no arguments to call once
+ * the condition is met.
+ * @param {string} [description] Error message to add to assert in case of
+ * failure.
+ * @param {number} timeout Timeout in ms. This is multiplied by the global
+ * timeout_multiplier
+ * @param {number} interval Polling interval in ms
+ *
+ */
+ Test.prototype.step_wait_func_done = function(cond, func, description,
+ timeout=3000, interval=100) {
+ this.step_wait_func(cond, () => {
+ if (func) {
+ func();
+ }
+ this.done();
+ }, description, timeout, interval);
+ };
+
+ /**
+ * Poll for a function to return true, and resolve a promise
+ * once it does, or assert if a timeout is reached. This is
+ * preferred over a simple step_timeout whenever possible
+ * since it allows the timeout to be longer to reduce
+ * intermittents without compromising test execution speed
+ * when the condition is quickly met.
+ *
+ * @example
+ * promise_test(async t => {
+ * // …
+ * await t.step_wait(() => frame.contentDocument === null, "Frame navigated to a cross-origin document");
+ * // …
+ * }, "");
+ *
+ * @param {Function} cond A function taking no arguments and
+ * returning a boolean.
+ * @param {string} [description] Error message to add to assert in case of
+ * failure.
+ * @param {number} timeout Timeout in ms. This is multiplied by the global
+ * timeout_multiplier
+ * @param {number} interval Polling interval in ms
+ * @returns {Promise} Promise resolved once cond is met.
+ *
+ */
+ Test.prototype.step_wait = function(cond, description, timeout=3000, interval=100) {
+ return new Promise(resolve => {
+ this.step_wait_func(cond, resolve, description, timeout, interval);
+ });
+ }
+
+ /*
+ * Private method for registering cleanup functions. `testharness.js`
+ * internals should use this method instead of the public `add_cleanup`
+ * method in order to hide implementation details from the harness status
+ * message in the case errors.
+ */
+ Test.prototype._add_cleanup = function(callback) {
+ this.cleanup_callbacks.push(callback);
+ };
+
+ /**
+ * Schedule a function to be run after the test result is known, regardless
+ * of passing or failing state.
+ *
+ * The behavior of this function will not
+ * influence the result of the test, but if an exception is thrown, the
+ * test harness will report an error.
+ *
+ * @param {Function} callback - The cleanup function to run. This
+ * is called with no arguments.
+ */
+ Test.prototype.add_cleanup = function(callback) {
+ this._user_defined_cleanup_count += 1;
+ this._add_cleanup(callback);
+ };
+
+ Test.prototype.set_timeout = function()
+ {
+ if (this.timeout_length !== null) {
+ var this_obj = this;
+ this.timeout_id = setTimeout(function()
+ {
+ this_obj.timeout();
+ }, this.timeout_length);
+ }
+ };
+
+ Test.prototype.set_status = function(status, message, stack)
+ {
+ this.status = status;
+ this.message = message;
+ this.stack = stack ? stack : null;
+ };
+
+ /**
+ * Manually set the test status to ``TIMEOUT``.
+ */
+ Test.prototype.timeout = function()
+ {
+ this.timeout_id = null;
+ this.set_status(this.TIMEOUT, "Test timed out");
+ this.phase = this.phases.HAS_RESULT;
+ this.done();
+ };
+
+ /**
+ * Manually set the test status to ``TIMEOUT``.
+ *
+ * Alias for `Test.timeout <#Test.timeout>`_.
+ */
+ Test.prototype.force_timeout = function() {
+ return this.timeout();
+ };
+
+ /**
+ * Mark the test as complete.
+ *
+ * This sets the test status to ``PASS`` if no other status was
+ * already recorded. Any subsequent attempts to run additional
+ * test steps will be ignored.
+ *
+ * After setting the test status any test cleanup functions will
+ * be run.
+ */
+ Test.prototype.done = function()
+ {
+ if (this.phase >= this.phases.CLEANING) {
+ return;
+ }
+
+ if (this.phase <= this.phases.STARTED) {
+ this.set_status(this.PASS, null);
+ }
+
+ if (global_scope.clearTimeout) {
+ clearTimeout(this.timeout_id);
+ }
+
+ if (settings.debug) {
+ console.log("TEST DONE",
+ this.status,
+ this.name);
+ }
+
+ this.cleanup();
+ };
+
+ function add_test_done_callback(test, callback)
+ {
+ if (test.phase === test.phases.COMPLETE) {
+ callback();
+ return;
+ }
+
+ test._done_callbacks.push(callback);
+ }
+
+ /*
+ * Invoke all specified cleanup functions. If one or more produce an error,
+ * the context is in an unpredictable state, so all further testing should
+ * be cancelled.
+ */
+ Test.prototype.cleanup = function() {
+ var errors = [];
+ var bad_value_count = 0;
+ function on_error(e) {
+ errors.push(e);
+ // Abort tests immediately so that tests declared within subsequent
+ // cleanup functions are not run.
+ tests.abort();
+ }
+ var this_obj = this;
+ var results = [];
+
+ this.phase = this.phases.CLEANING;
+
+ if (this._abortController) {
+ this._abortController.abort("Test cleanup");
+ }
+
+ forEach(this.cleanup_callbacks,
+ function(cleanup_callback) {
+ var result;
+
+ try {
+ result = cleanup_callback();
+ } catch (e) {
+ on_error(e);
+ return;
+ }
+
+ if (!is_valid_cleanup_result(this_obj, result)) {
+ bad_value_count += 1;
+ // Abort tests immediately so that tests declared
+ // within subsequent cleanup functions are not run.
+ tests.abort();
+ }
+
+ results.push(result);
+ });
+
+ if (!this._is_promise_test) {
+ cleanup_done(this_obj, errors, bad_value_count);
+ } else {
+ all_async(results,
+ function(result, done) {
+ if (result && typeof result.then === "function") {
+ result
+ .then(null, on_error)
+ .then(done);
+ } else {
+ done();
+ }
+ },
+ function() {
+ cleanup_done(this_obj, errors, bad_value_count);
+ });
+ }
+ };
+
+ /*
+ * Determine if the return value of a cleanup function is valid for a given
+ * test. Any test may return the value `undefined`. Tests created with
+ * `promise_test` may alternatively return "thenable" object values.
+ */
+ function is_valid_cleanup_result(test, result) {
+ if (result === undefined) {
+ return true;
+ }
+
+ if (test._is_promise_test) {
+ return result && typeof result.then === "function";
+ }
+
+ return false;
+ }
+
+ function cleanup_done(test, errors, bad_value_count) {
+ if (errors.length || bad_value_count) {
+ var total = test._user_defined_cleanup_count;
+
+ tests.status.status = tests.status.ERROR;
+ tests.status.stack = null;
+ tests.status.message = "Test named '" + test.name +
+ "' specified " + total +
+ " 'cleanup' function" + (total > 1 ? "s" : "");
+
+ if (errors.length) {
+ tests.status.message += ", and " + errors.length + " failed";
+ tests.status.stack = ((typeof errors[0] === "object" &&
+ errors[0].hasOwnProperty("stack")) ?
+ errors[0].stack : null);
+ }
+
+ if (bad_value_count) {
+ var type = test._is_promise_test ?
+ "non-thenable" : "non-undefined";
+ tests.status.message += ", and " + bad_value_count +
+ " returned a " + type + " value";
+ }
+
+ tests.status.message += ".";
+ }
+
+ test.phase = test.phases.COMPLETE;
+ tests.result(test);
+ forEach(test._done_callbacks,
+ function(callback) {
+ callback();
+ });
+ test._done_callbacks.length = 0;
+ }
+
+ /**
+ * Gives an AbortSignal that will be aborted when the test finishes.
+ */
+ Test.prototype.get_signal = function() {
+ if (!this._abortController) {
+ throw new Error("AbortController is not supported in this browser");
+ }
+ return this._abortController.signal;
+ }
+
+ /**
+ * A RemoteTest object mirrors a Test object on a remote worker. The
+ * associated RemoteWorker updates the RemoteTest object in response to
+ * received events. In turn, the RemoteTest object replicates these events
+ * on the local document. This allows listeners (test result reporting
+ * etc..) to transparently handle local and remote events.
+ */
+ function RemoteTest(clone) {
+ var this_obj = this;
+ Object.keys(clone).forEach(
+ function(key) {
+ this_obj[key] = clone[key];
+ });
+ this.index = null;
+ this.phase = this.phases.INITIAL;
+ this.update_state_from(clone);
+ this._done_callbacks = [];
+ tests.push(this);
+ }
+
+ RemoteTest.prototype.structured_clone = function() {
+ var clone = {};
+ Object.keys(this).forEach(
+ (function(key) {
+ var value = this[key];
+ // `RemoteTest` instances are responsible for managing
+ // their own "done" callback functions, so those functions
+ // are not relevant in other execution contexts. Because of
+ // this (and because Function values cannot be serialized
+ // for cross-realm transmittance), the property should not
+ // be considered when cloning instances.
+ if (key === '_done_callbacks' ) {
+ return;
+ }
+
+ if (typeof value === "object" && value !== null) {
+ clone[key] = merge({}, value);
+ } else {
+ clone[key] = value;
+ }
+ }).bind(this));
+ clone.phases = merge({}, this.phases);
+ return clone;
+ };
+
+ /**
+ * `RemoteTest` instances are objects which represent tests running in
+ * another realm. They do not define "cleanup" functions (if necessary,
+ * such functions are defined on the associated `Test` instance within the
+ * external realm). However, `RemoteTests` may have "done" callbacks (e.g.
+ * as attached by the `Tests` instance responsible for tracking the overall
+ * test status in the parent realm). The `cleanup` method delegates to
+ * `done` in order to ensure that such callbacks are invoked following the
+ * completion of the `RemoteTest`.
+ */
+ RemoteTest.prototype.cleanup = function() {
+ this.done();
+ };
+ RemoteTest.prototype.phases = Test.prototype.phases;
+ RemoteTest.prototype.update_state_from = function(clone) {
+ this.status = clone.status;
+ this.message = clone.message;
+ this.stack = clone.stack;
+ if (this.phase === this.phases.INITIAL) {
+ this.phase = this.phases.STARTED;
+ }
+ };
+ RemoteTest.prototype.done = function() {
+ this.phase = this.phases.COMPLETE;
+
+ forEach(this._done_callbacks,
+ function(callback) {
+ callback();
+ });
+ }
+
+ RemoteTest.prototype.format_status = function() {
+ return Test.prototype.status_formats[this.status];
+ }
+
+ /*
+ * A RemoteContext listens for test events from a remote test context, such
+ * as another window or a worker. These events are then used to construct
+ * and maintain RemoteTest objects that mirror the tests running in the
+ * remote context.
+ *
+ * An optional third parameter can be used as a predicate to filter incoming
+ * MessageEvents.
+ */
+ function RemoteContext(remote, message_target, message_filter) {
+ this.running = true;
+ this.started = false;
+ this.tests = new Array();
+ this.early_exception = null;
+
+ var this_obj = this;
+ // If remote context is cross origin assigning to onerror is not
+ // possible, so silently catch those errors.
+ try {
+ remote.onerror = function(error) { this_obj.remote_error(error); };
+ } catch (e) {
+ // Ignore.
+ }
+
+ // Keeping a reference to the remote object and the message handler until
+ // remote_done() is seen prevents the remote object and its message channel
+ // from going away before all the messages are dispatched.
+ this.remote = remote;
+ this.message_target = message_target;
+ this.message_handler = function(message) {
+ var passesFilter = !message_filter || message_filter(message);
+ // The reference to the `running` property in the following
+ // condition is unnecessary because that value is only set to
+ // `false` after the `message_handler` function has been
+ // unsubscribed.
+ // TODO: Simplify the condition by removing the reference.
+ if (this_obj.running && message.data && passesFilter &&
+ (message.data.type in this_obj.message_handlers)) {
+ this_obj.message_handlers[message.data.type].call(this_obj, message.data);
+ }
+ };
+
+ if (self.Promise) {
+ this.done = new Promise(function(resolve) {
+ this_obj.doneResolve = resolve;
+ });
+ }
+
+ this.message_target.addEventListener("message", this.message_handler);
+ }
+
+ RemoteContext.prototype.remote_error = function(error) {
+ if (error.preventDefault) {
+ error.preventDefault();
+ }
+
+ // Defer interpretation of errors until the testing protocol has
+ // started and the remote test's `allow_uncaught_exception` property
+ // is available.
+ if (!this.started) {
+ this.early_exception = error;
+ } else if (!this.allow_uncaught_exception) {
+ this.report_uncaught(error);
+ }
+ };
+
+ RemoteContext.prototype.report_uncaught = function(error) {
+ var message = error.message || String(error);
+ var filename = (error.filename ? " " + error.filename: "");
+ // FIXME: Display remote error states separately from main document
+ // error state.
+ tests.set_status(tests.status.ERROR,
+ "Error in remote" + filename + ": " + message,
+ error.stack);
+ };
+
+ RemoteContext.prototype.start = function(data) {
+ this.started = true;
+ this.allow_uncaught_exception = data.properties.allow_uncaught_exception;
+
+ if (this.early_exception && !this.allow_uncaught_exception) {
+ this.report_uncaught(this.early_exception);
+ }
+ };
+
+ RemoteContext.prototype.test_state = function(data) {
+ var remote_test = this.tests[data.test.index];
+ if (!remote_test) {
+ remote_test = new RemoteTest(data.test);
+ this.tests[data.test.index] = remote_test;
+ }
+ remote_test.update_state_from(data.test);
+ tests.notify_test_state(remote_test);
+ };
+
+ RemoteContext.prototype.test_done = function(data) {
+ var remote_test = this.tests[data.test.index];
+ remote_test.update_state_from(data.test);
+ remote_test.done();
+ tests.result(remote_test);
+ };
+
+ RemoteContext.prototype.remote_done = function(data) {
+ if (tests.status.status === null &&
+ data.status.status !== data.status.OK) {
+ tests.set_status(data.status.status, data.status.message, data.status.stack);
+ }
+
+ for (let assert of data.asserts) {
+ var record = new AssertRecord();
+ record.assert_name = assert.assert_name;
+ record.args = assert.args;
+ record.test = assert.test != null ? this.tests[assert.test.index] : null;
+ record.status = assert.status;
+ record.stack = assert.stack;
+ tests.asserts_run.push(record);
+ }
+
+ this.message_target.removeEventListener("message", this.message_handler);
+ this.running = false;
+
+ // If remote context is cross origin assigning to onerror is not
+ // possible, so silently catch those errors.
+ try {
+ this.remote.onerror = null;
+ } catch (e) {
+ // Ignore.
+ }
+
+ this.remote = null;
+ this.message_target = null;
+ if (this.doneResolve) {
+ this.doneResolve();
+ }
+
+ if (tests.all_done()) {
+ tests.complete();
+ }
+ };
+
+ RemoteContext.prototype.message_handlers = {
+ start: RemoteContext.prototype.start,
+ test_state: RemoteContext.prototype.test_state,
+ result: RemoteContext.prototype.test_done,
+ complete: RemoteContext.prototype.remote_done
+ };
+
+ /**
+ * @class
+ * Status of the overall harness
+ */
+ function TestsStatus()
+ {
+ /** The status code */
+ this.status = null;
+ /** Message in case of failure */
+ this.message = null;
+ /** Stack trace in case of an exception. */
+ this.stack = null;
+ }
+
+ /**
+ * Enum of possible harness statuses.
+ *
+ * :values:
+ * - ``OK``
+ * - ``ERROR``
+ * - ``TIMEOUT``
+ * - ``PRECONDITION_FAILED``
+ */
+ TestsStatus.statuses = {
+ OK:0,
+ ERROR:1,
+ TIMEOUT:2,
+ PRECONDITION_FAILED:3
+ };
+
+ TestsStatus.prototype = merge({}, TestsStatus.statuses);
+
+ TestsStatus.prototype.formats = {
+ 0: "OK",
+ 1: "Error",
+ 2: "Timeout",
+ 3: "Optional Feature Unsupported"
+ };
+
+ TestsStatus.prototype.structured_clone = function()
+ {
+ if (!this._structured_clone) {
+ var msg = this.message;
+ msg = msg ? String(msg) : msg;
+ this._structured_clone = merge({
+ status:this.status,
+ message:msg,
+ stack:this.stack
+ }, TestsStatus.statuses);
+ }
+ return this._structured_clone;
+ };
+
+ TestsStatus.prototype.format_status = function() {
+ return this.formats[this.status];
+ };
+
+ /**
+ * @class
+ * Record of an assert that ran.
+ *
+ * @param {Test} test - The test which ran the assert.
+ * @param {string} assert_name - The function name of the assert.
+ * @param {Any} args - The arguments passed to the assert function.
+ */
+ function AssertRecord(test, assert_name, args = []) {
+ /** Name of the assert that ran */
+ this.assert_name = assert_name;
+ /** Test that ran the assert */
+ this.test = test;
+ // Avoid keeping complex objects alive
+ /** Stringification of the arguments that were passed to the assert function */
+ this.args = args.map(x => format_value(x).replace(/\n/g, " "));
+ /** Status of the assert */
+ this.status = null;
+ }
+
+ AssertRecord.prototype.structured_clone = function() {
+ return {
+ assert_name: this.assert_name,
+ test: this.test ? this.test.structured_clone() : null,
+ args: this.args,
+ status: this.status,
+ };
+ };
+
+ function Tests()
+ {
+ this.tests = [];
+ this.num_pending = 0;
+
+ this.phases = {
+ INITIAL:0,
+ SETUP:1,
+ HAVE_TESTS:2,
+ HAVE_RESULTS:3,
+ COMPLETE:4
+ };
+ this.phase = this.phases.INITIAL;
+
+ this.properties = {};
+
+ this.wait_for_finish = false;
+ this.processing_callbacks = false;
+
+ this.allow_uncaught_exception = false;
+
+ this.file_is_test = false;
+ // This value is lazily initialized in order to avoid introducing a
+ // dependency on ECMAScript 2015 Promises to all tests.
+ this.promise_tests = null;
+ this.promise_setup_called = false;
+
+ this.timeout_multiplier = 1;
+ this.timeout_length = test_environment.test_timeout();
+ this.timeout_id = null;
+
+ this.start_callbacks = [];
+ this.test_state_callbacks = [];
+ this.test_done_callbacks = [];
+ this.all_done_callbacks = [];
+
+ this.hide_test_state = false;
+ this.pending_remotes = [];
+
+ this.current_test = null;
+ this.asserts_run = [];
+
+ // Track whether output is enabled, and thus whether or not we should
+ // track asserts.
+ //
+ // On workers we don't get properties set from testharnessreport.js, so
+ // we don't know whether or not to track asserts. To avoid the
+ // resulting performance hit, we assume we are not meant to. This means
+ // that assert tracking does not function on workers.
+ this.output = settings.output && 'document' in global_scope;
+
+ this.status = new TestsStatus();
+
+ var this_obj = this;
+
+ test_environment.add_on_loaded_callback(function() {
+ if (this_obj.all_done()) {
+ this_obj.complete();
+ }
+ });
+
+ this.set_timeout();
+ }
+
+ Tests.prototype.setup = function(func, properties)
+ {
+ if (this.phase >= this.phases.HAVE_RESULTS) {
+ return;
+ }
+
+ if (this.phase < this.phases.SETUP) {
+ this.phase = this.phases.SETUP;
+ }
+
+ this.properties = properties;
+
+ for (var p in properties) {
+ if (properties.hasOwnProperty(p)) {
+ var value = properties[p];
+ if (p == "allow_uncaught_exception") {
+ this.allow_uncaught_exception = value;
+ } else if (p == "explicit_done" && value) {
+ this.wait_for_finish = true;
+ } else if (p == "explicit_timeout" && value) {
+ this.timeout_length = null;
+ if (this.timeout_id)
+ {
+ clearTimeout(this.timeout_id);
+ }
+ } else if (p == "single_test" && value) {
+ this.set_file_is_test();
+ } else if (p == "timeout_multiplier") {
+ this.timeout_multiplier = value;
+ if (this.timeout_length) {
+ this.timeout_length *= this.timeout_multiplier;
+ }
+ } else if (p == "hide_test_state") {
+ this.hide_test_state = value;
+ } else if (p == "output") {
+ this.output = value;
+ } else if (p === "debug") {
+ settings.debug = value;
+ }
+ }
+ }
+
+ if (func) {
+ try {
+ func();
+ } catch (e) {
+ this.status.status = e instanceof OptionalFeatureUnsupportedError ? this.status.PRECONDITION_FAILED : this.status.ERROR;
+ this.status.message = String(e);
+ this.status.stack = e.stack ? e.stack : null;
+ this.complete();
+ }
+ }
+ this.set_timeout();
+ };
+
+ Tests.prototype.set_file_is_test = function() {
+ if (this.tests.length > 0) {
+ throw new Error("Tried to set file as test after creating a test");
+ }
+ this.wait_for_finish = true;
+ this.file_is_test = true;
+ // Create the test, which will add it to the list of tests
+ tests.current_test = async_test();
+ };
+
+ Tests.prototype.set_status = function(status, message, stack)
+ {
+ this.status.status = status;
+ this.status.message = message;
+ this.status.stack = stack ? stack : null;
+ };
+
+ Tests.prototype.set_timeout = function() {
+ if (global_scope.clearTimeout) {
+ var this_obj = this;
+ clearTimeout(this.timeout_id);
+ if (this.timeout_length !== null) {
+ this.timeout_id = setTimeout(function() {
+ this_obj.timeout();
+ }, this.timeout_length);
+ }
+ }
+ };
+
+ Tests.prototype.timeout = function() {
+ var test_in_cleanup = null;
+
+ if (this.status.status === null) {
+ forEach(this.tests,
+ function(test) {
+ // No more than one test is expected to be in the
+ // "CLEANUP" phase at any time
+ if (test.phase === test.phases.CLEANING) {
+ test_in_cleanup = test;
+ }
+
+ test.phase = test.phases.COMPLETE;
+ });
+
+ // Timeouts that occur while a test is in the "cleanup" phase
+ // indicate that some global state was not properly reverted. This
+ // invalidates the overall test execution, so the timeout should be
+ // reported as an error and cancel the execution of any remaining
+ // tests.
+ if (test_in_cleanup) {
+ this.status.status = this.status.ERROR;
+ this.status.message = "Timeout while running cleanup for " +
+ "test named \"" + test_in_cleanup.name + "\".";
+ tests.status.stack = null;
+ } else {
+ this.status.status = this.status.TIMEOUT;
+ }
+ }
+
+ this.complete();
+ };
+
+ Tests.prototype.end_wait = function()
+ {
+ this.wait_for_finish = false;
+ if (this.all_done()) {
+ this.complete();
+ }
+ };
+
+ Tests.prototype.push = function(test)
+ {
+ if (this.phase < this.phases.HAVE_TESTS) {
+ this.start();
+ }
+ this.num_pending++;
+ test.index = this.tests.push(test);
+ this.notify_test_state(test);
+ };
+
+ Tests.prototype.notify_test_state = function(test) {
+ var this_obj = this;
+ forEach(this.test_state_callbacks,
+ function(callback) {
+ callback(test, this_obj);
+ });
+ };
+
+ Tests.prototype.all_done = function() {
+ return (this.tests.length > 0 || this.pending_remotes.length > 0) &&
+ test_environment.all_loaded &&
+ (this.num_pending === 0 || this.is_aborted) && !this.wait_for_finish &&
+ !this.processing_callbacks &&
+ !this.pending_remotes.some(function(w) { return w.running; });
+ };
+
+ Tests.prototype.start = function() {
+ this.phase = this.phases.HAVE_TESTS;
+ this.notify_start();
+ };
+
+ Tests.prototype.notify_start = function() {
+ var this_obj = this;
+ forEach (this.start_callbacks,
+ function(callback)
+ {
+ callback(this_obj.properties);
+ });
+ };
+
+ Tests.prototype.result = function(test)
+ {
+ // If the harness has already transitioned beyond the `HAVE_RESULTS`
+ // phase, subsequent tests should not cause it to revert.
+ if (this.phase <= this.phases.HAVE_RESULTS) {
+ this.phase = this.phases.HAVE_RESULTS;
+ }
+ this.num_pending--;
+ this.notify_result(test);
+ };
+
+ Tests.prototype.notify_result = function(test) {
+ var this_obj = this;
+ this.processing_callbacks = true;
+ forEach(this.test_done_callbacks,
+ function(callback)
+ {
+ callback(test, this_obj);
+ });
+ this.processing_callbacks = false;
+ if (this_obj.all_done()) {
+ this_obj.complete();
+ }
+ };
+
+ Tests.prototype.complete = function() {
+ if (this.phase === this.phases.COMPLETE) {
+ return;
+ }
+ var this_obj = this;
+ var all_complete = function() {
+ this_obj.phase = this_obj.phases.COMPLETE;
+ this_obj.notify_complete();
+ };
+ var incomplete = filter(this.tests,
+ function(test) {
+ return test.phase < test.phases.COMPLETE;
+ });
+
+ /**
+ * To preserve legacy behavior, overall test completion must be
+ * signaled synchronously.
+ */
+ if (incomplete.length === 0) {
+ all_complete();
+ return;
+ }
+
+ all_async(incomplete,
+ function(test, testDone)
+ {
+ if (test.phase === test.phases.INITIAL) {
+ test.phase = test.phases.COMPLETE;
+ testDone();
+ } else {
+ add_test_done_callback(test, testDone);
+ test.cleanup();
+ }
+ },
+ all_complete);
+ };
+
+ Tests.prototype.set_assert = function(assert_name, args) {
+ this.asserts_run.push(new AssertRecord(this.current_test, assert_name, args))
+ }
+
+ Tests.prototype.set_assert_status = function(index, status, stack) {
+ let assert_record = this.asserts_run[index];
+ assert_record.status = status;
+ assert_record.stack = stack;
+ }
+
+ /**
+ * Update the harness status to reflect an unrecoverable harness error that
+ * should cancel all further testing. Update all previously-defined tests
+ * which have not yet started to indicate that they will not be executed.
+ */
+ Tests.prototype.abort = function() {
+ this.status.status = this.status.ERROR;
+ this.is_aborted = true;
+
+ forEach(this.tests,
+ function(test) {
+ if (test.phase === test.phases.INITIAL) {
+ test.phase = test.phases.COMPLETE;
+ }
+ });
+ };
+
+ /*
+ * Determine if any tests share the same `name` property. Return an array
+ * containing the names of any such duplicates.
+ */
+ Tests.prototype.find_duplicates = function() {
+ var names = Object.create(null);
+ var duplicates = [];
+
+ forEach (this.tests,
+ function(test)
+ {
+ if (test.name in names && duplicates.indexOf(test.name) === -1) {
+ duplicates.push(test.name);
+ }
+ names[test.name] = true;
+ });
+
+ return duplicates;
+ };
+
+ function code_unit_str(char) {
+ return 'U+' + char.charCodeAt(0).toString(16);
+ }
+
+ function sanitize_unpaired_surrogates(str) {
+ return str.replace(
+ /([\ud800-\udbff]+)(?![\udc00-\udfff])|(^|[^\ud800-\udbff])([\udc00-\udfff]+)/g,
+ function(_, low, prefix, high) {
+ var output = prefix || ""; // prefix may be undefined
+ var string = low || high; // only one of these alternates can match
+ for (var i = 0; i < string.length; i++) {
+ output += code_unit_str(string[i]);
+ }
+ return output;
+ });
+ }
+
+ function sanitize_all_unpaired_surrogates(tests) {
+ forEach (tests,
+ function (test)
+ {
+ var sanitized = sanitize_unpaired_surrogates(test.name);
+
+ if (test.name !== sanitized) {
+ test.name = sanitized;
+ delete test._structured_clone;
+ }
+ });
+ }
+
+ Tests.prototype.notify_complete = function() {
+ var this_obj = this;
+ var duplicates;
+
+ if (this.status.status === null) {
+ duplicates = this.find_duplicates();
+
+ // Some transports adhere to UTF-8's restriction on unpaired
+ // surrogates. Sanitize the titles so that the results can be
+ // consistently sent via all transports.
+ sanitize_all_unpaired_surrogates(this.tests);
+
+ // Test names are presumed to be unique within test files--this
+ // allows consumers to use them for identification purposes.
+ // Duplicated names violate this expectation and should therefore
+ // be reported as an error.
+ if (duplicates.length) {
+ this.status.status = this.status.ERROR;
+ this.status.message =
+ duplicates.length + ' duplicate test name' +
+ (duplicates.length > 1 ? 's' : '') + ': "' +
+ duplicates.join('", "') + '"';
+ } else {
+ this.status.status = this.status.OK;
+ }
+ }
+
+ forEach (this.all_done_callbacks,
+ function(callback)
+ {
+ callback(this_obj.tests, this_obj.status, this_obj.asserts_run);
+ });
+ };
+
+ /*
+ * Constructs a RemoteContext that tracks tests from a specific worker.
+ */
+ Tests.prototype.create_remote_worker = function(worker) {
+ var message_port;
+
+ if (is_service_worker(worker)) {
+ message_port = navigator.serviceWorker;
+ worker.postMessage({type: "connect"});
+ } else if (is_shared_worker(worker)) {
+ message_port = worker.port;
+ message_port.start();
+ } else {
+ message_port = worker;
+ }
+
+ return new RemoteContext(worker, message_port);
+ };
+
+ /*
+ * Constructs a RemoteContext that tracks tests from a specific window.
+ */
+ Tests.prototype.create_remote_window = function(remote) {
+ remote.postMessage({type: "getmessages"}, "*");
+ return new RemoteContext(
+ remote,
+ window,
+ function(msg) {
+ return msg.source === remote;
+ }
+ );
+ };
+
+ Tests.prototype.fetch_tests_from_worker = function(worker) {
+ if (this.phase >= this.phases.COMPLETE) {
+ return;
+ }
+
+ var remoteContext = this.create_remote_worker(worker);
+ this.pending_remotes.push(remoteContext);
+ return remoteContext.done;
+ };
+
+ /**
+ * Get test results from a worker and include them in the current test.
+ *
+ * @param {Worker|SharedWorker|ServiceWorker|MessagePort} port -
+ * Either a worker object or a port connected to a worker which is
+ * running tests..
+ * @returns {Promise} - A promise that's resolved once all the remote tests are complete.
+ */
+ function fetch_tests_from_worker(port) {
+ return tests.fetch_tests_from_worker(port);
+ }
+ expose(fetch_tests_from_worker, 'fetch_tests_from_worker');
+
+ Tests.prototype.fetch_tests_from_window = function(remote) {
+ if (this.phase >= this.phases.COMPLETE) {
+ return;
+ }
+
+ var remoteContext = this.create_remote_window(remote);
+ this.pending_remotes.push(remoteContext);
+ return remoteContext.done;
+ };
+
+ /**
+ * Aggregate tests from separate windows or iframes
+ * into the current document as if they were all part of the same test file.
+ *
+ * The document of the second window (or iframe) should include
+ * ``testharness.js``, but not ``testharnessreport.js``, and use
+ * :js:func:`test`, :js:func:`async_test`, and :js:func:`promise_test` in
+ * the usual manner.
+ *
+ * @param {Window} window - The window to fetch tests from.
+ */
+ function fetch_tests_from_window(window) {
+ return tests.fetch_tests_from_window(window);
+ }
+ expose(fetch_tests_from_window, 'fetch_tests_from_window');
+
+ /**
+ * Get test results from a shadow realm and include them in the current test.
+ *
+ * @param {ShadowRealm} realm - A shadow realm also running the test harness
+ * @returns {Promise} - A promise that's resolved once all the remote tests are complete.
+ */
+ function fetch_tests_from_shadow_realm(realm) {
+ var chan = new MessageChannel();
+ function receiveMessage(msg_json) {
+ chan.port1.postMessage(JSON.parse(msg_json));
+ }
+ var done = tests.fetch_tests_from_worker(chan.port2);
+ realm.evaluate("begin_shadow_realm_tests")(receiveMessage);
+ chan.port2.start();
+ return done;
+ }
+ expose(fetch_tests_from_shadow_realm, 'fetch_tests_from_shadow_realm');
+
+ /**
+ * Begin running tests in this shadow realm test harness.
+ *
+ * To be called after all tests have been loaded; it is an error to call
+ * this more than once or in a non-Shadow Realm environment
+ *
+ * @param {Function} postMessage - A function to send test updates to the
+ * incubating realm-- accepts JSON-encoded messages in the format used by
+ * RemoteContext
+ */
+ function begin_shadow_realm_tests(postMessage) {
+ if (!(test_environment instanceof ShadowRealmTestEnvironment)) {
+ throw new Error("begin_shadow_realm_tests called in non-Shadow Realm environment");
+ }
+
+ test_environment.begin(function (msg) {
+ postMessage(JSON.stringify(msg));
+ });
+ }
+ expose(begin_shadow_realm_tests, 'begin_shadow_realm_tests');
+
+ /**
+ * Timeout the tests.
+ *
+ * This only has an effect when ``explicit_timeout`` has been set
+ * in :js:func:`setup`. In other cases any call is a no-op.
+ *
+ */
+ function timeout() {
+ if (tests.timeout_length === null) {
+ tests.timeout();
+ }
+ }
+ expose(timeout, 'timeout');
+
+ /**
+ * Add a callback that's triggered when the first :js:class:`Test` is created.
+ *
+ * @param {Function} callback - Callback function. This is called
+ * without arguments.
+ */
+ function add_start_callback(callback) {
+ tests.start_callbacks.push(callback);
+ }
+
+ /**
+ * Add a callback that's triggered when a test state changes.
+ *
+ * @param {Function} callback - Callback function, called with the
+ * :js:class:`Test` as the only argument.
+ */
+ function add_test_state_callback(callback) {
+ tests.test_state_callbacks.push(callback);
+ }
+
+ /**
+ * Add a callback that's triggered when a test result is received.
+ *
+ * @param {Function} callback - Callback function, called with the
+ * :js:class:`Test` as the only argument.
+ */
+ function add_result_callback(callback) {
+ tests.test_done_callbacks.push(callback);
+ }
+
+ /**
+ * Add a callback that's triggered when all tests are complete.
+ *
+ * @param {Function} callback - Callback function, called with an
+ * array of :js:class:`Test` objects, a :js:class:`TestsStatus`
+ * object and an array of :js:class:`AssertRecord` objects. If the
+ * debug setting is ``false`` the final argument will be an empty
+ * array.
+ *
+ * For performance reasons asserts are only tracked when the debug
+ * setting is ``true``. In other cases the array of asserts will be
+ * empty.
+ */
+ function add_completion_callback(callback) {
+ tests.all_done_callbacks.push(callback);
+ }
+
+ expose(add_start_callback, 'add_start_callback');
+ expose(add_test_state_callback, 'add_test_state_callback');
+ expose(add_result_callback, 'add_result_callback');
+ expose(add_completion_callback, 'add_completion_callback');
+
+ function remove(array, item) {
+ var index = array.indexOf(item);
+ if (index > -1) {
+ array.splice(index, 1);
+ }
+ }
+
+ function remove_start_callback(callback) {
+ remove(tests.start_callbacks, callback);
+ }
+
+ function remove_test_state_callback(callback) {
+ remove(tests.test_state_callbacks, callback);
+ }
+
+ function remove_result_callback(callback) {
+ remove(tests.test_done_callbacks, callback);
+ }
+
+ function remove_completion_callback(callback) {
+ remove(tests.all_done_callbacks, callback);
+ }
+
+ /*
+ * Output listener
+ */
+
+ function Output() {
+ this.output_document = document;
+ this.output_node = null;
+ this.enabled = settings.output;
+ this.phase = this.INITIAL;
+ }
+
+ Output.prototype.INITIAL = 0;
+ Output.prototype.STARTED = 1;
+ Output.prototype.HAVE_RESULTS = 2;
+ Output.prototype.COMPLETE = 3;
+
+ Output.prototype.setup = function(properties) {
+ if (this.phase > this.INITIAL) {
+ return;
+ }
+
+ //If output is disabled in testharnessreport.js the test shouldn't be
+ //able to override that
+ this.enabled = this.enabled && (properties.hasOwnProperty("output") ?
+ properties.output : settings.output);
+ };
+
+ Output.prototype.init = function(properties) {
+ if (this.phase >= this.STARTED) {
+ return;
+ }
+ if (properties.output_document) {
+ this.output_document = properties.output_document;
+ } else {
+ this.output_document = document;
+ }
+ this.phase = this.STARTED;
+ };
+
+ Output.prototype.resolve_log = function() {
+ var output_document;
+ if (this.output_node) {
+ return;
+ }
+ if (typeof this.output_document === "function") {
+ output_document = this.output_document.apply(undefined);
+ } else {
+ output_document = this.output_document;
+ }
+ if (!output_document) {
+ return;
+ }
+ var node = output_document.getElementById("log");
+ if (!node) {
+ if (output_document.readyState === "loading") {
+ return;
+ }
+ node = output_document.createElementNS("http://www.w3.org/1999/xhtml", "div");
+ node.id = "log";
+ if (output_document.body) {
+ output_document.body.appendChild(node);
+ } else {
+ var root = output_document.documentElement;
+ var is_html = (root &&
+ root.namespaceURI == "http://www.w3.org/1999/xhtml" &&
+ root.localName == "html");
+ var is_svg = (output_document.defaultView &&
+ "SVGSVGElement" in output_document.defaultView &&
+ root instanceof output_document.defaultView.SVGSVGElement);
+ if (is_svg) {
+ var foreignObject = output_document.createElementNS("http://www.w3.org/2000/svg", "foreignObject");
+ foreignObject.setAttribute("width", "100%");
+ foreignObject.setAttribute("height", "100%");
+ root.appendChild(foreignObject);
+ foreignObject.appendChild(node);
+ } else if (is_html) {
+ root.appendChild(output_document.createElementNS("http://www.w3.org/1999/xhtml", "body"))
+ .appendChild(node);
+ } else {
+ root.appendChild(node);
+ }
+ }
+ }
+ this.output_document = output_document;
+ this.output_node = node;
+ };
+
+ Output.prototype.show_status = function() {
+ if (this.phase < this.STARTED) {
+ this.init({});
+ }
+ if (!this.enabled || this.phase === this.COMPLETE) {
+ return;
+ }
+ this.resolve_log();
+ if (this.phase < this.HAVE_RESULTS) {
+ this.phase = this.HAVE_RESULTS;
+ }
+ var done_count = tests.tests.length - tests.num_pending;
+ if (this.output_node && !tests.hide_test_state) {
+ if (done_count < 100 ||
+ (done_count < 1000 && done_count % 100 === 0) ||
+ done_count % 1000 === 0) {
+ this.output_node.textContent = "Running, " +
+ done_count + " complete, " +
+ tests.num_pending + " remain";
+ }
+ }
+ };
+
+ Output.prototype.show_results = function (tests, harness_status, asserts_run) {
+ if (this.phase >= this.COMPLETE) {
+ return;
+ }
+ if (!this.enabled) {
+ return;
+ }
+ if (!this.output_node) {
+ this.resolve_log();
+ }
+ this.phase = this.COMPLETE;
+
+ var log = this.output_node;
+ if (!log) {
+ return;
+ }
+ var output_document = this.output_document;
+
+ while (log.lastChild) {
+ log.removeChild(log.lastChild);
+ }
+
+ var stylesheet = output_document.createElementNS(xhtml_ns, "style");
+ stylesheet.textContent = stylesheetContent;
+ var heads = output_document.getElementsByTagName("head");
+ if (heads.length) {
+ heads[0].appendChild(stylesheet);
+ }
+
+ var status_number = {};
+ forEach(tests,
+ function(test) {
+ var status = test.format_status();
+ if (status_number.hasOwnProperty(status)) {
+ status_number[status] += 1;
+ } else {
+ status_number[status] = 1;
+ }
+ });
+
+ function status_class(status)
+ {
+ return status.replace(/\s/g, '').toLowerCase();
+ }
+
+ var summary_template = ["section", {"id":"summary"},
+ ["h2", {}, "Summary"],
+ function()
+ {
+ var status = harness_status.format_status();
+ var rv = [["section", {},
+ ["p", {},
+ "Harness status: ",
+ ["span", {"class":status_class(status)},
+ status
+ ],
+ ],
+ ["button",
+ {"onclick": "let evt = new Event('__test_restart'); " +
+ "let canceled = !window.dispatchEvent(evt);" +
+ "if (!canceled) { location.reload() }"},
+ "Rerun"]
+ ]];
+
+ if (harness_status.status === harness_status.ERROR) {
+ rv[0].push(["pre", {}, harness_status.message]);
+ if (harness_status.stack) {
+ rv[0].push(["pre", {}, harness_status.stack]);
+ }
+ }
+ return rv;
+ },
+ ["p", {}, "Found ${num_tests} tests"],
+ function() {
+ var rv = [["div", {}]];
+ var i = 0;
+ while (Test.prototype.status_formats.hasOwnProperty(i)) {
+ if (status_number.hasOwnProperty(Test.prototype.status_formats[i])) {
+ var status = Test.prototype.status_formats[i];
+ rv[0].push(["div", {},
+ ["label", {},
+ ["input", {type:"checkbox", checked:"checked"}],
+ status_number[status] + " ",
+ ["span", {"class":status_class(status)}, status]]]);
+ }
+ i++;
+ }
+ return rv;
+ },
+ ];
+
+ log.appendChild(render(summary_template, {num_tests:tests.length}, output_document));
+
+ forEach(output_document.querySelectorAll("section#summary label"),
+ function(element)
+ {
+ on_event(element, "click",
+ function(e)
+ {
+ if (output_document.getElementById("results") === null) {
+ e.preventDefault();
+ return;
+ }
+ var result_class = element.querySelector("span[class]").getAttribute("class");
+ var style_element = output_document.querySelector("style#hide-" + result_class);
+ var input_element = element.querySelector("input");
+ if (!style_element && !input_element.checked) {
+ style_element = output_document.createElementNS(xhtml_ns, "style");
+ style_element.id = "hide-" + result_class;
+ style_element.textContent = "table#results > tbody > tr.overall-"+result_class+"{display:none}";
+ output_document.body.appendChild(style_element);
+ } else if (style_element && input_element.checked) {
+ style_element.parentNode.removeChild(style_element);
+ }
+ });
+ });
+
+ // This use of innerHTML plus manual escaping is not recommended in
+ // general, but is necessary here for performance. Using textContent
+ // on each individual <td> adds tens of seconds of execution time for
+ // large test suites (tens of thousands of tests).
+ function escape_html(s)
+ {
+ return s.replace(/\&/g, "&amp;")
+ .replace(/</g, "&lt;")
+ .replace(/"/g, "&quot;")
+ .replace(/'/g, "&#39;");
+ }
+
+ function has_assertions()
+ {
+ for (var i = 0; i < tests.length; i++) {
+ if (tests[i].properties.hasOwnProperty("assert")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ function get_assertion(test)
+ {
+ if (test.properties.hasOwnProperty("assert")) {
+ if (Array.isArray(test.properties.assert)) {
+ return test.properties.assert.join(' ');
+ }
+ return test.properties.assert;
+ }
+ return '';
+ }
+
+ var asserts_run_by_test = new Map();
+ asserts_run.forEach(assert => {
+ if (!asserts_run_by_test.has(assert.test)) {
+ asserts_run_by_test.set(assert.test, []);
+ }
+ asserts_run_by_test.get(assert.test).push(assert);
+ });
+
+ function get_asserts_output(test) {
+ var asserts = asserts_run_by_test.get(test);
+ if (!asserts) {
+ return "No asserts ran";
+ }
+ rv = "<table>";
+ rv += asserts.map(assert => {
+ var output_fn = "<strong>" + escape_html(assert.assert_name) + "</strong>(";
+ var prefix_len = output_fn.length;
+ var output_args = assert.args;
+ var output_len = output_args.reduce((prev, current) => prev+current, prefix_len);
+ if (output_len[output_len.length - 1] > 50) {
+ output_args = output_args.map((x, i) =>
+ (i > 0 ? " ".repeat(prefix_len) : "" )+ x + (i < output_args.length - 1 ? ",\n" : ""));
+ } else {
+ output_args = output_args.map((x, i) => x + (i < output_args.length - 1 ? ", " : ""));
+ }
+ output_fn += escape_html(output_args.join(""));
+ output_fn += ')';
+ var output_location;
+ if (assert.stack) {
+ output_location = assert.stack.split("\n", 1)[0].replace(/@?\w+:\/\/[^ "\/]+(?::\d+)?/g, " ");
+ }
+ return "<tr class='overall-" +
+ status_class(Test.prototype.status_formats[assert.status]) + "'>" +
+ "<td class='" +
+ status_class(Test.prototype.status_formats[assert.status]) + "'>" +
+ Test.prototype.status_formats[assert.status] + "</td>" +
+ "<td><pre>" +
+ output_fn +
+ (output_location ? "\n" + escape_html(output_location) : "") +
+ "</pre></td></tr>";
+ }
+ ).join("\n");
+ rv += "</table>";
+ return rv;
+ }
+
+ log.appendChild(document.createElementNS(xhtml_ns, "section"));
+ var assertions = has_assertions();
+ var html = "<h2>Details</h2><table id='results' " + (assertions ? "class='assertions'" : "" ) + ">" +
+ "<thead><tr><th>Result</th><th>Test Name</th>" +
+ (assertions ? "<th>Assertion</th>" : "") +
+ "<th>Message</th></tr></thead>" +
+ "<tbody>";
+ for (var i = 0; i < tests.length; i++) {
+ var test = tests[i];
+ html += '<tr class="overall-' +
+ status_class(test.format_status()) +
+ '">' +
+ '<td class="' +
+ status_class(test.format_status()) +
+ '">' +
+ test.format_status() +
+ "</td><td>" +
+ escape_html(test.name) +
+ "</td><td>" +
+ (assertions ? escape_html(get_assertion(test)) + "</td><td>" : "") +
+ escape_html(test.message ? tests[i].message : " ") +
+ (tests[i].stack ? "<pre>" +
+ escape_html(tests[i].stack) +
+ "</pre>": "");
+ if (!(test instanceof RemoteTest)) {
+ html += "<details><summary>Asserts run</summary>" + get_asserts_output(test) + "</details>"
+ }
+ html += "</td></tr>";
+ }
+ html += "</tbody></table>";
+ try {
+ log.lastChild.innerHTML = html;
+ } catch (e) {
+ log.appendChild(document.createElementNS(xhtml_ns, "p"))
+ .textContent = "Setting innerHTML for the log threw an exception.";
+ log.appendChild(document.createElementNS(xhtml_ns, "pre"))
+ .textContent = html;
+ }
+ };
+
+ /*
+ * Template code
+ *
+ * A template is just a JavaScript structure. An element is represented as:
+ *
+ * [tag_name, {attr_name:attr_value}, child1, child2]
+ *
+ * the children can either be strings (which act like text nodes), other templates or
+ * functions (see below)
+ *
+ * A text node is represented as
+ *
+ * ["{text}", value]
+ *
+ * String values have a simple substitution syntax; ${foo} represents a variable foo.
+ *
+ * It is possible to embed logic in templates by using a function in a place where a
+ * node would usually go. The function must either return part of a template or null.
+ *
+ * In cases where a set of nodes are required as output rather than a single node
+ * with children it is possible to just use a list
+ * [node1, node2, node3]
+ *
+ * Usage:
+ *
+ * render(template, substitutions) - take a template and an object mapping
+ * variable names to parameters and return either a DOM node or a list of DOM nodes
+ *
+ * substitute(template, substitutions) - take a template and variable mapping object,
+ * make the variable substitutions and return the substituted template
+ *
+ */
+
+ function is_single_node(template)
+ {
+ return typeof template[0] === "string";
+ }
+
+ function substitute(template, substitutions)
+ {
+ if (typeof template === "function") {
+ var replacement = template(substitutions);
+ if (!replacement) {
+ return null;
+ }
+
+ return substitute(replacement, substitutions);
+ }
+
+ if (is_single_node(template)) {
+ return substitute_single(template, substitutions);
+ }
+
+ return filter(map(template, function(x) {
+ return substitute(x, substitutions);
+ }), function(x) {return x !== null;});
+ }
+
+ function substitute_single(template, substitutions)
+ {
+ var substitution_re = /\$\{([^ }]*)\}/g;
+
+ function do_substitution(input) {
+ var components = input.split(substitution_re);
+ var rv = [];
+ for (var i = 0; i < components.length; i += 2) {
+ rv.push(components[i]);
+ if (components[i + 1]) {
+ rv.push(String(substitutions[components[i + 1]]));
+ }
+ }
+ return rv;
+ }
+
+ function substitute_attrs(attrs, rv)
+ {
+ rv[1] = {};
+ for (var name in template[1]) {
+ if (attrs.hasOwnProperty(name)) {
+ var new_name = do_substitution(name).join("");
+ var new_value = do_substitution(attrs[name]).join("");
+ rv[1][new_name] = new_value;
+ }
+ }
+ }
+
+ function substitute_children(children, rv)
+ {
+ for (var i = 0; i < children.length; i++) {
+ if (children[i] instanceof Object) {
+ var replacement = substitute(children[i], substitutions);
+ if (replacement !== null) {
+ if (is_single_node(replacement)) {
+ rv.push(replacement);
+ } else {
+ extend(rv, replacement);
+ }
+ }
+ } else {
+ extend(rv, do_substitution(String(children[i])));
+ }
+ }
+ return rv;
+ }
+
+ var rv = [];
+ rv.push(do_substitution(String(template[0])).join(""));
+
+ if (template[0] === "{text}") {
+ substitute_children(template.slice(1), rv);
+ } else {
+ substitute_attrs(template[1], rv);
+ substitute_children(template.slice(2), rv);
+ }
+
+ return rv;
+ }
+
+ function make_dom_single(template, doc)
+ {
+ var output_document = doc || document;
+ var element;
+ if (template[0] === "{text}") {
+ element = output_document.createTextNode("");
+ for (var i = 1; i < template.length; i++) {
+ element.data += template[i];
+ }
+ } else {
+ element = output_document.createElementNS(xhtml_ns, template[0]);
+ for (var name in template[1]) {
+ if (template[1].hasOwnProperty(name)) {
+ element.setAttribute(name, template[1][name]);
+ }
+ }
+ for (var i = 2; i < template.length; i++) {
+ if (template[i] instanceof Object) {
+ var sub_element = make_dom(template[i]);
+ element.appendChild(sub_element);
+ } else {
+ var text_node = output_document.createTextNode(template[i]);
+ element.appendChild(text_node);
+ }
+ }
+ }
+
+ return element;
+ }
+
+ function make_dom(template, substitutions, output_document)
+ {
+ if (is_single_node(template)) {
+ return make_dom_single(template, output_document);
+ }
+
+ return map(template, function(x) {
+ return make_dom_single(x, output_document);
+ });
+ }
+
+ function render(template, substitutions, output_document)
+ {
+ return make_dom(substitute(template, substitutions), output_document);
+ }
+
+ /*
+ * Utility functions
+ */
+ function assert(expected_true, function_name, description, error, substitutions)
+ {
+ if (expected_true !== true) {
+ var msg = make_message(function_name, description,
+ error, substitutions);
+ throw new AssertionError(msg);
+ }
+ }
+
+ /**
+ * @class
+ * Exception type that represents a failing assert.
+ *
+ * @param {string} message - Error message.
+ */
+ function AssertionError(message)
+ {
+ if (typeof message == "string") {
+ message = sanitize_unpaired_surrogates(message);
+ }
+ this.message = message;
+ this.stack = get_stack();
+ }
+ expose(AssertionError, "AssertionError");
+
+ AssertionError.prototype = Object.create(Error.prototype);
+
+ const get_stack = function() {
+ var stack = new Error().stack;
+
+ // 'Error.stack' is not supported in all browsers/versions
+ if (!stack) {
+ return "(Stack trace unavailable)";
+ }
+
+ var lines = stack.split("\n");
+
+ // Create a pattern to match stack frames originating within testharness.js. These include the
+ // script URL, followed by the line/col (e.g., '/resources/testharness.js:120:21').
+ // Escape the URL per http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
+ // in case it contains RegExp characters.
+ var script_url = get_script_url();
+ var re_text = script_url ? script_url.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') : "\\btestharness.js";
+ var re = new RegExp(re_text + ":\\d+:\\d+");
+
+ // Some browsers include a preamble that specifies the type of the error object. Skip this by
+ // advancing until we find the first stack frame originating from testharness.js.
+ var i = 0;
+ while (!re.test(lines[i]) && i < lines.length) {
+ i++;
+ }
+
+ // Then skip the top frames originating from testharness.js to begin the stack at the test code.
+ while (re.test(lines[i]) && i < lines.length) {
+ i++;
+ }
+
+ // Paranoid check that we didn't skip all frames. If so, return the original stack unmodified.
+ if (i >= lines.length) {
+ return stack;
+ }
+
+ return lines.slice(i).join("\n");
+ }
+
+ function OptionalFeatureUnsupportedError(message)
+ {
+ AssertionError.call(this, message);
+ }
+ OptionalFeatureUnsupportedError.prototype = Object.create(AssertionError.prototype);
+ expose(OptionalFeatureUnsupportedError, "OptionalFeatureUnsupportedError");
+
+ function make_message(function_name, description, error, substitutions)
+ {
+ for (var p in substitutions) {
+ if (substitutions.hasOwnProperty(p)) {
+ substitutions[p] = format_value(substitutions[p]);
+ }
+ }
+ var node_form = substitute(["{text}", "${function_name}: ${description}" + error],
+ merge({function_name:function_name,
+ description:(description?description + " ":"")},
+ substitutions));
+ return node_form.slice(1).join("");
+ }
+
+ function filter(array, callable, thisObj) {
+ var rv = [];
+ for (var i = 0; i < array.length; i++) {
+ if (array.hasOwnProperty(i)) {
+ var pass = callable.call(thisObj, array[i], i, array);
+ if (pass) {
+ rv.push(array[i]);
+ }
+ }
+ }
+ return rv;
+ }
+
+ function map(array, callable, thisObj)
+ {
+ var rv = [];
+ rv.length = array.length;
+ for (var i = 0; i < array.length; i++) {
+ if (array.hasOwnProperty(i)) {
+ rv[i] = callable.call(thisObj, array[i], i, array);
+ }
+ }
+ return rv;
+ }
+
+ function extend(array, items)
+ {
+ Array.prototype.push.apply(array, items);
+ }
+
+ function forEach(array, callback, thisObj)
+ {
+ for (var i = 0; i < array.length; i++) {
+ if (array.hasOwnProperty(i)) {
+ callback.call(thisObj, array[i], i, array);
+ }
+ }
+ }
+
+ /**
+ * Immediately invoke a "iteratee" function with a series of values in
+ * parallel and invoke a final "done" function when all of the "iteratee"
+ * invocations have signaled completion.
+ *
+ * If all callbacks complete synchronously (or if no callbacks are
+ * specified), the ``done_callback`` will be invoked synchronously. It is the
+ * responsibility of the caller to ensure asynchronicity in cases where
+ * that is desired.
+ *
+ * @param {array} value Zero or more values to use in the invocation of
+ * ``iter_callback``
+ * @param {function} iter_callback A function that will be invoked
+ * once for each of the values min
+ * ``value``. Two arguments will
+ * be available in each
+ * invocation: the value from
+ * ``value`` and a function that
+ * must be invoked to signal
+ * completion
+ * @param {function} done_callback A function that will be invoked after
+ * all operations initiated by the
+ * ``iter_callback`` function have signaled
+ * completion
+ */
+ function all_async(values, iter_callback, done_callback)
+ {
+ var remaining = values.length;
+
+ if (remaining === 0) {
+ done_callback();
+ }
+
+ forEach(values,
+ function(element) {
+ var invoked = false;
+ var elDone = function() {
+ if (invoked) {
+ return;
+ }
+
+ invoked = true;
+ remaining -= 1;
+
+ if (remaining === 0) {
+ done_callback();
+ }
+ };
+
+ iter_callback(element, elDone);
+ });
+ }
+
+ function merge(a,b)
+ {
+ var rv = {};
+ var p;
+ for (p in a) {
+ rv[p] = a[p];
+ }
+ for (p in b) {
+ rv[p] = b[p];
+ }
+ return rv;
+ }
+
+ function expose(object, name)
+ {
+ var components = name.split(".");
+ var target = global_scope;
+ for (var i = 0; i < components.length - 1; i++) {
+ if (!(components[i] in target)) {
+ target[components[i]] = {};
+ }
+ target = target[components[i]];
+ }
+ target[components[components.length - 1]] = object;
+ }
+
+ function is_same_origin(w) {
+ try {
+ 'random_prop' in w;
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ /** Returns the 'src' URL of the first <script> tag in the page to include the file 'testharness.js'. */
+ function get_script_url()
+ {
+ if (!('document' in global_scope)) {
+ return undefined;
+ }
+
+ var scripts = document.getElementsByTagName("script");
+ for (var i = 0; i < scripts.length; i++) {
+ var src;
+ if (scripts[i].src) {
+ src = scripts[i].src;
+ } else if (scripts[i].href) {
+ //SVG case
+ src = scripts[i].href.baseVal;
+ }
+
+ var matches = src && src.match(/^(.*\/|)testharness\.js$/);
+ if (matches) {
+ return src;
+ }
+ }
+ return undefined;
+ }
+
+ /** Returns the <title> or filename or "Untitled" */
+ function get_title()
+ {
+ if ('document' in global_scope) {
+ //Don't use document.title to work around an Opera/Presto bug in XHTML documents
+ var title = document.getElementsByTagName("title")[0];
+ if (title && title.firstChild && title.firstChild.data) {
+ return title.firstChild.data;
+ }
+ }
+ if ('META_TITLE' in global_scope && META_TITLE) {
+ return META_TITLE;
+ }
+ if ('location' in global_scope && 'pathname' in location) {
+ return location.pathname.substring(location.pathname.lastIndexOf('/') + 1, location.pathname.indexOf('.'));
+ }
+ return "Untitled";
+ }
+
+ /**
+ * Setup globals
+ */
+
+ var tests = new Tests();
+
+ if (global_scope.addEventListener) {
+ var error_handler = function(error, message, stack) {
+ var optional_unsupported = error instanceof OptionalFeatureUnsupportedError;
+ if (tests.file_is_test) {
+ var test = tests.tests[0];
+ if (test.phase >= test.phases.HAS_RESULT) {
+ return;
+ }
+ var status = optional_unsupported ? test.PRECONDITION_FAILED : test.FAIL;
+ test.set_status(status, message, stack);
+ test.phase = test.phases.HAS_RESULT;
+ } else if (!tests.allow_uncaught_exception) {
+ var status = optional_unsupported ? tests.status.PRECONDITION_FAILED : tests.status.ERROR;
+ tests.status.status = status;
+ tests.status.message = message;
+ tests.status.stack = stack;
+ }
+
+ // Do not transition to the "complete" phase if the test has been
+ // configured to allow uncaught exceptions. This gives the test an
+ // opportunity to define subtests based on the exception reporting
+ // behavior.
+ if (!tests.allow_uncaught_exception) {
+ done();
+ }
+ };
+
+ addEventListener("error", function(e) {
+ var message = e.message;
+ var stack;
+ if (e.error && e.error.stack) {
+ stack = e.error.stack;
+ } else {
+ stack = e.filename + ":" + e.lineno + ":" + e.colno;
+ }
+ error_handler(e.error, message, stack);
+ }, false);
+
+ addEventListener("unhandledrejection", function(e) {
+ var message;
+ if (e.reason && e.reason.message) {
+ message = "Unhandled rejection: " + e.reason.message;
+ } else {
+ message = "Unhandled rejection";
+ }
+ var stack;
+ if (e.reason && e.reason.stack) {
+ stack = e.reason.stack;
+ }
+ error_handler(e.reason, message, stack);
+ }, false);
+ }
+
+ test_environment.on_tests_ready();
+
+ /**
+ * Stylesheet
+ */
+ var stylesheetContent = "\
+html {\
+ font-family:DejaVu Sans, Bitstream Vera Sans, Arial, Sans;\
+}\
+\
+#log .warning,\
+#log .warning a {\
+ color: black;\
+ background: yellow;\
+}\
+\
+#log .error,\
+#log .error a {\
+ color: white;\
+ background: red;\
+}\
+\
+section#summary {\
+ margin-bottom:1em;\
+}\
+\
+table#results {\
+ border-collapse:collapse;\
+ table-layout:fixed;\
+ width:100%;\
+}\
+\
+table#results > thead > tr > th:first-child,\
+table#results > tbody > tr > td:first-child {\
+ width:8em;\
+}\
+\
+table#results > thead > tr > th:last-child,\
+table#results > thead > tr > td:last-child {\
+ width:50%;\
+}\
+\
+table#results.assertions > thead > tr > th:last-child,\
+table#results.assertions > tbody > tr > td:last-child {\
+ width:35%;\
+}\
+\
+table#results > thead > > tr > th {\
+ padding:0;\
+ padding-bottom:0.5em;\
+ border-bottom:medium solid black;\
+}\
+\
+table#results > tbody > tr> td {\
+ padding:1em;\
+ padding-bottom:0.5em;\
+ border-bottom:thin solid black;\
+}\
+\
+.pass {\
+ color:green;\
+}\
+\
+.fail {\
+ color:red;\
+}\
+\
+tr.timeout {\
+ color:red;\
+}\
+\
+tr.notrun {\
+ color:blue;\
+}\
+\
+tr.optionalunsupported {\
+ color:blue;\
+}\
+\
+.ok {\
+ color:green;\
+}\
+\
+.error {\
+ color:red;\
+}\
+\
+.pass, .fail, .timeout, .notrun, .optionalunsupported .ok, .timeout, .error {\
+ font-variant:small-caps;\
+}\
+\
+table#results span {\
+ display:block;\
+}\
+\
+table#results span.expected {\
+ font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace;\
+ white-space:pre;\
+}\
+\
+table#results span.actual {\
+ font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace;\
+ white-space:pre;\
+}\
+";
+
+})(self);
+// vim: set expandtab shiftwidth=4 tabstop=4:
diff --git a/testing/web-platform/tests/resources/testharness.js.headers b/testing/web-platform/tests/resources/testharness.js.headers
new file mode 100644
index 0000000000..5e8f640c66
--- /dev/null
+++ b/testing/web-platform/tests/resources/testharness.js.headers
@@ -0,0 +1,2 @@
+Content-Type: text/javascript; charset=utf-8
+Cache-Control: max-age=3600
diff --git a/testing/web-platform/tests/resources/testharnessreport.js b/testing/web-platform/tests/resources/testharnessreport.js
new file mode 100644
index 0000000000..e5cb40fe0e
--- /dev/null
+++ b/testing/web-platform/tests/resources/testharnessreport.js
@@ -0,0 +1,57 @@
+/* global add_completion_callback */
+/* global setup */
+
+/*
+ * This file is intended for vendors to implement code needed to integrate
+ * testharness.js tests with their own test systems.
+ *
+ * Typically test system integration will attach callbacks when each test has
+ * run, using add_result_callback(callback(test)), or when the whole test file
+ * has completed, using
+ * add_completion_callback(callback(tests, harness_status)).
+ *
+ * For more documentation about the callback functions and the
+ * parameters they are called with see testharness.js
+ */
+
+function dump_test_results(tests, status) {
+ var results_element = document.createElement("script");
+ results_element.type = "text/json";
+ results_element.id = "__testharness__results__";
+ var test_results = tests.map(function(x) {
+ return {name:x.name, status:x.status, message:x.message, stack:x.stack}
+ });
+ var data = {test:window.location.href,
+ tests:test_results,
+ status: status.status,
+ message: status.message,
+ stack: status.stack};
+ results_element.textContent = JSON.stringify(data);
+
+ // To avoid a HierarchyRequestError with XML documents, ensure that 'results_element'
+ // is inserted at a location that results in a valid document.
+ var parent = document.body
+ ? document.body // <body> is required in XHTML documents
+ : document.documentElement; // fallback for optional <body> in HTML5, SVG, etc.
+
+ parent.appendChild(results_element);
+}
+
+add_completion_callback(dump_test_results);
+
+/* If the parent window has a testharness_properties object,
+ * we use this to provide the test settings. This is used by the
+ * default in-browser runner to configure the timeout and the
+ * rendering of results
+ */
+try {
+ if (window.opener && "testharness_properties" in window.opener) {
+ /* If we pass the testharness_properties object as-is here without
+ * JSON stringifying and reparsing it, IE fails & emits the message
+ * "Could not complete the operation due to error 80700019".
+ */
+ setup(JSON.parse(JSON.stringify(window.opener.testharness_properties)));
+ }
+} catch (e) {
+}
+// vim: set expandtab shiftwidth=4 tabstop=4:
diff --git a/testing/web-platform/tests/resources/testharnessreport.js.headers b/testing/web-platform/tests/resources/testharnessreport.js.headers
new file mode 100644
index 0000000000..5e8f640c66
--- /dev/null
+++ b/testing/web-platform/tests/resources/testharnessreport.js.headers
@@ -0,0 +1,2 @@
+Content-Type: text/javascript; charset=utf-8
+Cache-Control: max-age=3600
diff --git a/testing/web-platform/tests/resources/webidl2/build.sh b/testing/web-platform/tests/resources/webidl2/build.sh
new file mode 100755
index 0000000000..a631268224
--- /dev/null
+++ b/testing/web-platform/tests/resources/webidl2/build.sh
@@ -0,0 +1,12 @@
+set -ex
+
+if [ ! -d "webidl2.js" ]; then
+ git clone https://github.com/w3c/webidl2.js.git
+fi
+cd webidl2.js
+npm install
+npm run build-debug
+HASH=$(git rev-parse HEAD)
+cd ..
+cp webidl2.js/dist/webidl2.js lib/
+echo "Currently using webidl2.js@${HASH}." > lib/VERSION.md
diff --git a/testing/web-platform/tests/resources/webidl2/lib/README.md b/testing/web-platform/tests/resources/webidl2/lib/README.md
new file mode 100644
index 0000000000..1bd583269d
--- /dev/null
+++ b/testing/web-platform/tests/resources/webidl2/lib/README.md
@@ -0,0 +1,4 @@
+This directory contains a built version of the [webidl2.js library](https://github.com/w3c/webidl2.js).
+It is built by running `npm run build-debug` at the root of that repository.
+
+The `webidl2.js.headers` file is a local addition to ensure the script is interpreted as UTF-8.
diff --git a/testing/web-platform/tests/resources/webidl2/lib/VERSION.md b/testing/web-platform/tests/resources/webidl2/lib/VERSION.md
new file mode 100644
index 0000000000..5a3726c6c0
--- /dev/null
+++ b/testing/web-platform/tests/resources/webidl2/lib/VERSION.md
@@ -0,0 +1 @@
+Currently using webidl2.js@6889aee6fc7d65915ab1267825248157dbc50486.
diff --git a/testing/web-platform/tests/resources/webidl2/lib/webidl2.js b/testing/web-platform/tests/resources/webidl2/lib/webidl2.js
new file mode 100644
index 0000000000..7161def899
--- /dev/null
+++ b/testing/web-platform/tests/resources/webidl2/lib/webidl2.js
@@ -0,0 +1,3824 @@
+(function webpackUniversalModuleDefinition(root, factory) {
+ if(typeof exports === 'object' && typeof module === 'object')
+ module.exports = factory();
+ else if(typeof define === 'function' && define.amd)
+ define([], factory);
+ else if(typeof exports === 'object')
+ exports["WebIDL2"] = factory();
+ else
+ root["WebIDL2"] = factory();
+})(globalThis, () => {
+return /******/ (() => { // webpackBootstrap
+/******/ "use strict";
+/******/ var __webpack_modules__ = ([
+/* 0 */,
+/* 1 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "parse": () => (/* binding */ parse)
+/* harmony export */ });
+/* harmony import */ var _tokeniser_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
+/* harmony import */ var _productions_enum_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(15);
+/* harmony import */ var _productions_includes_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(16);
+/* harmony import */ var _productions_extended_attributes_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(8);
+/* harmony import */ var _productions_typedef_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(17);
+/* harmony import */ var _productions_callback_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(18);
+/* harmony import */ var _productions_interface_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(19);
+/* harmony import */ var _productions_mixin_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(25);
+/* harmony import */ var _productions_dictionary_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(26);
+/* harmony import */ var _productions_namespace_js__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(28);
+/* harmony import */ var _productions_callback_interface_js__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(29);
+/* harmony import */ var _productions_helpers_js__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(4);
+/* harmony import */ var _productions_token_js__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(10);
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+/**
+ * @param {Tokeniser} tokeniser
+ * @param {object} options
+ * @param {boolean} [options.concrete]
+ * @param {Function[]} [options.productions]
+ */
+function parseByTokens(tokeniser, options) {
+ const source = tokeniser.source;
+
+ function error(str) {
+ tokeniser.error(str);
+ }
+
+ function consume(...candidates) {
+ return tokeniser.consume(...candidates);
+ }
+
+ function callback() {
+ const callback = consume("callback");
+ if (!callback) return;
+ if (tokeniser.probe("interface")) {
+ return _productions_callback_interface_js__WEBPACK_IMPORTED_MODULE_10__.CallbackInterface.parse(tokeniser, callback);
+ }
+ return _productions_callback_js__WEBPACK_IMPORTED_MODULE_5__.CallbackFunction.parse(tokeniser, callback);
+ }
+
+ function interface_(opts) {
+ const base = consume("interface");
+ if (!base) return;
+ const ret =
+ _productions_mixin_js__WEBPACK_IMPORTED_MODULE_7__.Mixin.parse(tokeniser, base, opts) ||
+ _productions_interface_js__WEBPACK_IMPORTED_MODULE_6__.Interface.parse(tokeniser, base, opts) ||
+ error("Interface has no proper body");
+ return ret;
+ }
+
+ function partial() {
+ const partial = consume("partial");
+ if (!partial) return;
+ return (
+ _productions_dictionary_js__WEBPACK_IMPORTED_MODULE_8__.Dictionary.parse(tokeniser, { partial }) ||
+ interface_({ partial }) ||
+ _productions_namespace_js__WEBPACK_IMPORTED_MODULE_9__.Namespace.parse(tokeniser, { partial }) ||
+ error("Partial doesn't apply to anything")
+ );
+ }
+
+ function definition() {
+ if (options.productions) {
+ for (const production of options.productions) {
+ const result = production(tokeniser);
+ if (result) {
+ return result;
+ }
+ }
+ }
+
+ return (
+ callback() ||
+ interface_() ||
+ partial() ||
+ _productions_dictionary_js__WEBPACK_IMPORTED_MODULE_8__.Dictionary.parse(tokeniser) ||
+ _productions_enum_js__WEBPACK_IMPORTED_MODULE_1__.Enum.parse(tokeniser) ||
+ _productions_typedef_js__WEBPACK_IMPORTED_MODULE_4__.Typedef.parse(tokeniser) ||
+ _productions_includes_js__WEBPACK_IMPORTED_MODULE_2__.Includes.parse(tokeniser) ||
+ _productions_namespace_js__WEBPACK_IMPORTED_MODULE_9__.Namespace.parse(tokeniser)
+ );
+ }
+
+ function definitions() {
+ if (!source.length) return [];
+ const defs = [];
+ while (true) {
+ const ea = _productions_extended_attributes_js__WEBPACK_IMPORTED_MODULE_3__.ExtendedAttributes.parse(tokeniser);
+ const def = definition();
+ if (!def) {
+ if (ea.length) error("Stray extended attributes");
+ break;
+ }
+ (0,_productions_helpers_js__WEBPACK_IMPORTED_MODULE_11__.autoParenter)(def).extAttrs = ea;
+ defs.push(def);
+ }
+ const eof = _productions_token_js__WEBPACK_IMPORTED_MODULE_12__.Eof.parse(tokeniser);
+ if (options.concrete) {
+ defs.push(eof);
+ }
+ return defs;
+ }
+ const res = definitions();
+ if (tokeniser.position < source.length) error("Unrecognised tokens");
+ return res;
+}
+
+/**
+ * @param {string} str
+ * @param {object} [options]
+ * @param {*} [options.sourceName]
+ * @param {boolean} [options.concrete]
+ * @param {Function[]} [options.productions]
+ * @return {import("./productions/base.js").Base[]}
+ */
+function parse(str, options = {}) {
+ const tokeniser = new _tokeniser_js__WEBPACK_IMPORTED_MODULE_0__.Tokeniser(str);
+ if (typeof options.sourceName !== "undefined") {
+ // @ts-ignore (See Tokeniser.source in supplement.d.ts)
+ tokeniser.source.name = options.sourceName;
+ }
+ return parseByTokens(tokeniser, options);
+}
+
+
+/***/ }),
+/* 2 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Tokeniser": () => (/* binding */ Tokeniser),
+/* harmony export */ "WebIDLParseError": () => (/* binding */ WebIDLParseError),
+/* harmony export */ "argumentNameKeywords": () => (/* binding */ argumentNameKeywords),
+/* harmony export */ "stringTypes": () => (/* binding */ stringTypes),
+/* harmony export */ "typeNameKeywords": () => (/* binding */ typeNameKeywords)
+/* harmony export */ });
+/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3);
+/* harmony import */ var _productions_helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
+
+
+
+// These regular expressions use the sticky flag so they will only match at
+// the current location (ie. the offset of lastIndex).
+const tokenRe = {
+ // This expression uses a lookahead assertion to catch false matches
+ // against integers early.
+ decimal:
+ /-?(?=[0-9]*\.|[0-9]+[eE])(([0-9]+\.[0-9]*|[0-9]*\.[0-9]+)([Ee][-+]?[0-9]+)?|[0-9]+[Ee][-+]?[0-9]+)/y,
+ integer: /-?(0([Xx][0-9A-Fa-f]+|[0-7]*)|[1-9][0-9]*)/y,
+ identifier: /[_-]?[A-Za-z][0-9A-Z_a-z-]*/y,
+ string: /"[^"]*"/y,
+ whitespace: /[\t\n\r ]+/y,
+ comment: /\/\/.*|\/\*[\s\S]*?\*\//y,
+ other: /[^\t\n\r 0-9A-Za-z]/y,
+};
+
+const typeNameKeywords = [
+ "ArrayBuffer",
+ "DataView",
+ "Int8Array",
+ "Int16Array",
+ "Int32Array",
+ "Uint8Array",
+ "Uint16Array",
+ "Uint32Array",
+ "Uint8ClampedArray",
+ "BigInt64Array",
+ "BigUint64Array",
+ "Float32Array",
+ "Float64Array",
+ "any",
+ "object",
+ "symbol",
+];
+
+const stringTypes = ["ByteString", "DOMString", "USVString"];
+
+const argumentNameKeywords = [
+ "async",
+ "attribute",
+ "callback",
+ "const",
+ "constructor",
+ "deleter",
+ "dictionary",
+ "enum",
+ "getter",
+ "includes",
+ "inherit",
+ "interface",
+ "iterable",
+ "maplike",
+ "namespace",
+ "partial",
+ "required",
+ "setlike",
+ "setter",
+ "static",
+ "stringifier",
+ "typedef",
+ "unrestricted",
+];
+
+const nonRegexTerminals = [
+ "-Infinity",
+ "FrozenArray",
+ "Infinity",
+ "NaN",
+ "ObservableArray",
+ "Promise",
+ "bigint",
+ "boolean",
+ "byte",
+ "double",
+ "false",
+ "float",
+ "long",
+ "mixin",
+ "null",
+ "octet",
+ "optional",
+ "or",
+ "readonly",
+ "record",
+ "sequence",
+ "short",
+ "true",
+ "undefined",
+ "unsigned",
+ "void",
+].concat(argumentNameKeywords, stringTypes, typeNameKeywords);
+
+const punctuations = [
+ "(",
+ ")",
+ ",",
+ "...",
+ ":",
+ ";",
+ "<",
+ "=",
+ ">",
+ "?",
+ "*",
+ "[",
+ "]",
+ "{",
+ "}",
+];
+
+const reserved = [
+ // "constructor" is now a keyword
+ "_constructor",
+ "toString",
+ "_toString",
+];
+
+/**
+ * @typedef {ArrayItemType<ReturnType<typeof tokenise>>} Token
+ * @param {string} str
+ */
+function tokenise(str) {
+ const tokens = [];
+ let lastCharIndex = 0;
+ let trivia = "";
+ let line = 1;
+ let index = 0;
+ while (lastCharIndex < str.length) {
+ const nextChar = str.charAt(lastCharIndex);
+ let result = -1;
+
+ if (/[\t\n\r ]/.test(nextChar)) {
+ result = attemptTokenMatch("whitespace", { noFlushTrivia: true });
+ } else if (nextChar === "/") {
+ result = attemptTokenMatch("comment", { noFlushTrivia: true });
+ }
+
+ if (result !== -1) {
+ const currentTrivia = tokens.pop().value;
+ line += (currentTrivia.match(/\n/g) || []).length;
+ trivia += currentTrivia;
+ index -= 1;
+ } else if (/[-0-9.A-Z_a-z]/.test(nextChar)) {
+ result = attemptTokenMatch("decimal");
+ if (result === -1) {
+ result = attemptTokenMatch("integer");
+ }
+ if (result === -1) {
+ result = attemptTokenMatch("identifier");
+ const lastIndex = tokens.length - 1;
+ const token = tokens[lastIndex];
+ if (result !== -1) {
+ if (reserved.includes(token.value)) {
+ const message = `${(0,_productions_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(
+ token.value
+ )} is a reserved identifier and must not be used.`;
+ throw new WebIDLParseError(
+ (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.syntaxError)(tokens, lastIndex, null, message)
+ );
+ } else if (nonRegexTerminals.includes(token.value)) {
+ token.type = "inline";
+ }
+ }
+ }
+ } else if (nextChar === '"') {
+ result = attemptTokenMatch("string");
+ }
+
+ for (const punctuation of punctuations) {
+ if (str.startsWith(punctuation, lastCharIndex)) {
+ tokens.push({
+ type: "inline",
+ value: punctuation,
+ trivia,
+ line,
+ index,
+ });
+ trivia = "";
+ lastCharIndex += punctuation.length;
+ result = lastCharIndex;
+ break;
+ }
+ }
+
+ // other as the last try
+ if (result === -1) {
+ result = attemptTokenMatch("other");
+ }
+ if (result === -1) {
+ throw new Error("Token stream not progressing");
+ }
+ lastCharIndex = result;
+ index += 1;
+ }
+
+ // remaining trivia as eof
+ tokens.push({
+ type: "eof",
+ value: "",
+ trivia,
+ line,
+ index,
+ });
+
+ return tokens;
+
+ /**
+ * @param {keyof typeof tokenRe} type
+ * @param {object} options
+ * @param {boolean} [options.noFlushTrivia]
+ */
+ function attemptTokenMatch(type, { noFlushTrivia } = {}) {
+ const re = tokenRe[type];
+ re.lastIndex = lastCharIndex;
+ const result = re.exec(str);
+ if (result) {
+ tokens.push({ type, value: result[0], trivia, line, index });
+ if (!noFlushTrivia) {
+ trivia = "";
+ }
+ return re.lastIndex;
+ }
+ return -1;
+ }
+}
+
+class Tokeniser {
+ /**
+ * @param {string} idl
+ */
+ constructor(idl) {
+ this.source = tokenise(idl);
+ this.position = 0;
+ }
+
+ /**
+ * @param {string} message
+ * @return {never}
+ */
+ error(message) {
+ throw new WebIDLParseError(
+ (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.syntaxError)(this.source, this.position, this.current, message)
+ );
+ }
+
+ /**
+ * @param {string} type
+ */
+ probeKind(type) {
+ return (
+ this.source.length > this.position &&
+ this.source[this.position].type === type
+ );
+ }
+
+ /**
+ * @param {string} value
+ */
+ probe(value) {
+ return (
+ this.probeKind("inline") && this.source[this.position].value === value
+ );
+ }
+
+ /**
+ * @param {...string} candidates
+ */
+ consumeKind(...candidates) {
+ for (const type of candidates) {
+ if (!this.probeKind(type)) continue;
+ const token = this.source[this.position];
+ this.position++;
+ return token;
+ }
+ }
+
+ /**
+ * @param {...string} candidates
+ */
+ consume(...candidates) {
+ if (!this.probeKind("inline")) return;
+ const token = this.source[this.position];
+ for (const value of candidates) {
+ if (token.value !== value) continue;
+ this.position++;
+ return token;
+ }
+ }
+
+ /**
+ * @param {string} value
+ */
+ consumeIdentifier(value) {
+ if (!this.probeKind("identifier")) {
+ return;
+ }
+ if (this.source[this.position].value !== value) {
+ return;
+ }
+ return this.consumeKind("identifier");
+ }
+
+ /**
+ * @param {number} position
+ */
+ unconsume(position) {
+ this.position = position;
+ }
+}
+
+class WebIDLParseError extends Error {
+ /**
+ * @param {object} options
+ * @param {string} options.message
+ * @param {string} options.bareMessage
+ * @param {string} options.context
+ * @param {number} options.line
+ * @param {*} options.sourceName
+ * @param {string} options.input
+ * @param {*[]} options.tokens
+ */
+ constructor({
+ message,
+ bareMessage,
+ context,
+ line,
+ sourceName,
+ input,
+ tokens,
+ }) {
+ super(message);
+
+ this.name = "WebIDLParseError"; // not to be mangled
+ this.bareMessage = bareMessage;
+ this.context = context;
+ this.line = line;
+ this.sourceName = sourceName;
+ this.input = input;
+ this.tokens = tokens;
+ }
+}
+
+
+/***/ }),
+/* 3 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "syntaxError": () => (/* binding */ syntaxError),
+/* harmony export */ "validationError": () => (/* binding */ validationError)
+/* harmony export */ });
+/**
+ * @param {string} text
+ */
+function lastLine(text) {
+ const splitted = text.split("\n");
+ return splitted[splitted.length - 1];
+}
+
+function appendIfExist(base, target) {
+ let result = base;
+ if (target) {
+ result += ` ${target}`;
+ }
+ return result;
+}
+
+function contextAsText(node) {
+ const hierarchy = [node];
+ while (node && node.parent) {
+ const { parent } = node;
+ hierarchy.unshift(parent);
+ node = parent;
+ }
+ return hierarchy.map((n) => appendIfExist(n.type, n.name)).join(" -> ");
+}
+
+/**
+ * @typedef {object} WebIDL2ErrorOptions
+ * @property {"error" | "warning"} [level]
+ * @property {Function} [autofix]
+ * @property {string} [ruleName]
+ *
+ * @typedef {ReturnType<typeof error>} WebIDLErrorData
+ *
+ * @param {string} message error message
+ * @param {*} position
+ * @param {*} current
+ * @param {*} message
+ * @param {"Syntax" | "Validation"} kind error type
+ * @param {WebIDL2ErrorOptions=} options
+ */
+function error(
+ source,
+ position,
+ current,
+ message,
+ kind,
+ { level = "error", autofix, ruleName } = {}
+) {
+ /**
+ * @param {number} count
+ */
+ function sliceTokens(count) {
+ return count > 0
+ ? source.slice(position, position + count)
+ : source.slice(Math.max(position + count, 0), position);
+ }
+
+ /**
+ * @param {import("./tokeniser.js").Token[]} inputs
+ * @param {object} [options]
+ * @param {boolean} [options.precedes]
+ * @returns
+ */
+ function tokensToText(inputs, { precedes } = {}) {
+ const text = inputs.map((t) => t.trivia + t.value).join("");
+ const nextToken = source[position];
+ if (nextToken.type === "eof") {
+ return text;
+ }
+ if (precedes) {
+ return text + nextToken.trivia;
+ }
+ return text.slice(nextToken.trivia.length);
+ }
+
+ const maxTokens = 5; // arbitrary but works well enough
+ const line =
+ source[position].type !== "eof"
+ ? source[position].line
+ : source.length > 1
+ ? source[position - 1].line
+ : 1;
+
+ const precedingLastLine = lastLine(
+ tokensToText(sliceTokens(-maxTokens), { precedes: true })
+ );
+
+ const subsequentTokens = sliceTokens(maxTokens);
+ const subsequentText = tokensToText(subsequentTokens);
+ const subsequentFirstLine = subsequentText.split("\n")[0];
+
+ const spaced = " ".repeat(precedingLastLine.length) + "^";
+ const sourceContext = precedingLastLine + subsequentFirstLine + "\n" + spaced;
+
+ const contextType = kind === "Syntax" ? "since" : "inside";
+ const inSourceName = source.name ? ` in ${source.name}` : "";
+ const grammaticalContext =
+ current && current.name
+ ? `, ${contextType} \`${current.partial ? "partial " : ""}${contextAsText(
+ current
+ )}\``
+ : "";
+ const context = `${kind} error at line ${line}${inSourceName}${grammaticalContext}:\n${sourceContext}`;
+ return {
+ message: `${context} ${message}`,
+ bareMessage: message,
+ context,
+ line,
+ sourceName: source.name,
+ level,
+ ruleName,
+ autofix,
+ input: subsequentText,
+ tokens: subsequentTokens,
+ };
+}
+
+/**
+ * @param {string} message error message
+ */
+function syntaxError(source, position, current, message) {
+ return error(source, position, current, message, "Syntax");
+}
+
+/**
+ * @param {string} message error message
+ * @param {WebIDL2ErrorOptions} [options]
+ */
+function validationError(
+ token,
+ current,
+ ruleName,
+ message,
+ options = {}
+) {
+ options.ruleName = ruleName;
+ return error(
+ current.source,
+ token.index,
+ current,
+ message,
+ "Validation",
+ options
+ );
+}
+
+
+/***/ }),
+/* 4 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "argument_list": () => (/* binding */ argument_list),
+/* harmony export */ "autoParenter": () => (/* binding */ autoParenter),
+/* harmony export */ "autofixAddExposedWindow": () => (/* binding */ autofixAddExposedWindow),
+/* harmony export */ "const_data": () => (/* binding */ const_data),
+/* harmony export */ "const_value": () => (/* binding */ const_value),
+/* harmony export */ "findLastIndex": () => (/* binding */ findLastIndex),
+/* harmony export */ "getFirstToken": () => (/* binding */ getFirstToken),
+/* harmony export */ "getLastIndentation": () => (/* binding */ getLastIndentation),
+/* harmony export */ "getMemberIndentation": () => (/* binding */ getMemberIndentation),
+/* harmony export */ "list": () => (/* binding */ list),
+/* harmony export */ "primitive_type": () => (/* binding */ primitive_type),
+/* harmony export */ "return_type": () => (/* binding */ return_type),
+/* harmony export */ "stringifier": () => (/* binding */ stringifier),
+/* harmony export */ "type_with_extended_attributes": () => (/* binding */ type_with_extended_attributes),
+/* harmony export */ "unescape": () => (/* binding */ unescape)
+/* harmony export */ });
+/* harmony import */ var _type_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(5);
+/* harmony import */ var _argument_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(11);
+/* harmony import */ var _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(8);
+/* harmony import */ var _operation_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(13);
+/* harmony import */ var _attribute_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(14);
+/* harmony import */ var _tokeniser_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(2);
+
+
+
+
+
+
+
+/**
+ * @param {string} identifier
+ */
+function unescape(identifier) {
+ return identifier.startsWith("_") ? identifier.slice(1) : identifier;
+}
+
+/**
+ * Parses comma-separated list
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {object} args
+ * @param {Function} args.parser parser function for each item
+ * @param {boolean} [args.allowDangler] whether to allow dangling comma
+ * @param {string} [args.listName] the name to be shown on error messages
+ */
+function list(tokeniser, { parser, allowDangler, listName = "list" }) {
+ const first = parser(tokeniser);
+ if (!first) {
+ return [];
+ }
+ first.tokens.separator = tokeniser.consume(",");
+ const items = [first];
+ while (first.tokens.separator) {
+ const item = parser(tokeniser);
+ if (!item) {
+ if (!allowDangler) {
+ tokeniser.error(`Trailing comma in ${listName}`);
+ }
+ break;
+ }
+ item.tokens.separator = tokeniser.consume(",");
+ items.push(item);
+ if (!item.tokens.separator) break;
+ }
+ return items;
+}
+
+/**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+function const_value(tokeniser) {
+ return (
+ tokeniser.consumeKind("decimal", "integer") ||
+ tokeniser.consume("true", "false", "Infinity", "-Infinity", "NaN")
+ );
+}
+
+/**
+ * @param {object} token
+ * @param {string} token.type
+ * @param {string} token.value
+ */
+function const_data({ type, value }) {
+ switch (type) {
+ case "decimal":
+ case "integer":
+ return { type: "number", value };
+ case "string":
+ return { type: "string", value: value.slice(1, -1) };
+ }
+
+ switch (value) {
+ case "true":
+ case "false":
+ return { type: "boolean", value: value === "true" };
+ case "Infinity":
+ case "-Infinity":
+ return { type: "Infinity", negative: value.startsWith("-") };
+ case "[":
+ return { type: "sequence", value: [] };
+ case "{":
+ return { type: "dictionary" };
+ default:
+ return { type: value };
+ }
+}
+
+/**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+function primitive_type(tokeniser) {
+ function integer_type() {
+ const prefix = tokeniser.consume("unsigned");
+ const base = tokeniser.consume("short", "long");
+ if (base) {
+ const postfix = tokeniser.consume("long");
+ return new _type_js__WEBPACK_IMPORTED_MODULE_0__.Type({ source, tokens: { prefix, base, postfix } });
+ }
+ if (prefix) tokeniser.error("Failed to parse integer type");
+ }
+
+ function decimal_type() {
+ const prefix = tokeniser.consume("unrestricted");
+ const base = tokeniser.consume("float", "double");
+ if (base) {
+ return new _type_js__WEBPACK_IMPORTED_MODULE_0__.Type({ source, tokens: { prefix, base } });
+ }
+ if (prefix) tokeniser.error("Failed to parse float type");
+ }
+
+ const { source } = tokeniser;
+ const num_type = integer_type() || decimal_type();
+ if (num_type) return num_type;
+ const base = tokeniser.consume(
+ "bigint",
+ "boolean",
+ "byte",
+ "octet",
+ "undefined"
+ );
+ if (base) {
+ return new _type_js__WEBPACK_IMPORTED_MODULE_0__.Type({ source, tokens: { base } });
+ }
+}
+
+/**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+function argument_list(tokeniser) {
+ return list(tokeniser, {
+ parser: _argument_js__WEBPACK_IMPORTED_MODULE_1__.Argument.parse,
+ listName: "arguments list",
+ });
+}
+
+/**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {string=} typeName (TODO: See Type.type for more details)
+ */
+function type_with_extended_attributes(tokeniser, typeName) {
+ const extAttrs = _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__.ExtendedAttributes.parse(tokeniser);
+ const ret = _type_js__WEBPACK_IMPORTED_MODULE_0__.Type.parse(tokeniser, typeName);
+ if (ret) autoParenter(ret).extAttrs = extAttrs;
+ return ret;
+}
+
+/**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {string=} typeName (TODO: See Type.type for more details)
+ */
+function return_type(tokeniser, typeName) {
+ const typ = _type_js__WEBPACK_IMPORTED_MODULE_0__.Type.parse(tokeniser, typeName || "return-type");
+ if (typ) {
+ return typ;
+ }
+ const voidToken = tokeniser.consume("void");
+ if (voidToken) {
+ const ret = new _type_js__WEBPACK_IMPORTED_MODULE_0__.Type({
+ source: tokeniser.source,
+ tokens: { base: voidToken },
+ });
+ ret.type = "return-type";
+ return ret;
+ }
+}
+
+/**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+function stringifier(tokeniser) {
+ const special = tokeniser.consume("stringifier");
+ if (!special) return;
+ const member =
+ _attribute_js__WEBPACK_IMPORTED_MODULE_4__.Attribute.parse(tokeniser, { special }) ||
+ _operation_js__WEBPACK_IMPORTED_MODULE_3__.Operation.parse(tokeniser, { special }) ||
+ tokeniser.error("Unterminated stringifier");
+ return member;
+}
+
+/**
+ * @param {string} str
+ */
+function getLastIndentation(str) {
+ const lines = str.split("\n");
+ // the first line visually binds to the preceding token
+ if (lines.length) {
+ const match = lines[lines.length - 1].match(/^\s+/);
+ if (match) {
+ return match[0];
+ }
+ }
+ return "";
+}
+
+/**
+ * @param {string} parentTrivia
+ */
+function getMemberIndentation(parentTrivia) {
+ const indentation = getLastIndentation(parentTrivia);
+ const indentCh = indentation.includes("\t") ? "\t" : " ";
+ return indentation + indentCh;
+}
+
+/**
+ * @param {import("./interface.js").Interface} def
+ */
+function autofixAddExposedWindow(def) {
+ return () => {
+ if (def.extAttrs.length) {
+ const tokeniser = new _tokeniser_js__WEBPACK_IMPORTED_MODULE_5__.Tokeniser("Exposed=Window,");
+ const exposed = _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__.SimpleExtendedAttribute.parse(tokeniser);
+ exposed.tokens.separator = tokeniser.consume(",");
+ const existing = def.extAttrs[0];
+ if (!/^\s/.test(existing.tokens.name.trivia)) {
+ existing.tokens.name.trivia = ` ${existing.tokens.name.trivia}`;
+ }
+ def.extAttrs.unshift(exposed);
+ } else {
+ autoParenter(def).extAttrs = _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__.ExtendedAttributes.parse(
+ new _tokeniser_js__WEBPACK_IMPORTED_MODULE_5__.Tokeniser("[Exposed=Window]")
+ );
+ const trivia = def.tokens.base.trivia;
+ def.extAttrs.tokens.open.trivia = trivia;
+ def.tokens.base.trivia = `\n${getLastIndentation(trivia)}`;
+ }
+ };
+}
+
+/**
+ * Get the first syntax token for the given IDL object.
+ * @param {*} data
+ */
+function getFirstToken(data) {
+ if (data.extAttrs.length) {
+ return data.extAttrs.tokens.open;
+ }
+ if (data.type === "operation" && !data.special) {
+ return getFirstToken(data.idlType);
+ }
+ const tokens = Object.values(data.tokens).sort((x, y) => x.index - y.index);
+ return tokens[0];
+}
+
+/**
+ * @template T
+ * @param {T[]} array
+ * @param {(item: T) => boolean} predicate
+ */
+function findLastIndex(array, predicate) {
+ const index = array.slice().reverse().findIndex(predicate);
+ if (index === -1) {
+ return index;
+ }
+ return array.length - index - 1;
+}
+
+/**
+ * Returns a proxy that auto-assign `parent` field.
+ * @template {Record<string | symbol, any>} T
+ * @param {T} data
+ * @param {*} [parent] The object that will be assigned to `parent`.
+ * If absent, it will be `data` by default.
+ * @return {T}
+ */
+function autoParenter(data, parent) {
+ if (!parent) {
+ // Defaults to `data` unless specified otherwise.
+ parent = data;
+ }
+ if (!data) {
+ // This allows `autoParenter(undefined)` which again allows
+ // `autoParenter(parse())` where the function may return nothing.
+ return data;
+ }
+ const proxy = new Proxy(data, {
+ get(target, p) {
+ const value = target[p];
+ if (Array.isArray(value) && p !== "source") {
+ // Wraps the array so that any added items will also automatically
+ // get their `parent` values.
+ return autoParenter(value, target);
+ }
+ return value;
+ },
+ set(target, p, value) {
+ // @ts-ignore https://github.com/microsoft/TypeScript/issues/47357
+ target[p] = value;
+ if (!value) {
+ return true;
+ } else if (Array.isArray(value)) {
+ // Assigning an array will add `parent` to its items.
+ for (const item of value) {
+ if (typeof item.parent !== "undefined") {
+ item.parent = parent;
+ }
+ }
+ } else if (typeof value.parent !== "undefined") {
+ value.parent = parent;
+ }
+ return true;
+ },
+ });
+ return proxy;
+}
+
+
+/***/ }),
+/* 5 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Type": () => (/* binding */ Type)
+/* harmony export */ });
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
+/* harmony import */ var _tokeniser_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(2);
+/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(3);
+/* harmony import */ var _validators_helpers_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(7);
+/* harmony import */ var _extended_attributes_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(8);
+
+
+
+
+
+
+
+/**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {string} typeName
+ */
+function generic_type(tokeniser, typeName) {
+ const base = tokeniser.consume(
+ "FrozenArray",
+ "ObservableArray",
+ "Promise",
+ "sequence",
+ "record"
+ );
+ if (!base) {
+ return;
+ }
+ const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)(
+ new Type({ source: tokeniser.source, tokens: { base } })
+ );
+ ret.tokens.open =
+ tokeniser.consume("<") ||
+ tokeniser.error(`No opening bracket after ${base.value}`);
+ switch (base.value) {
+ case "Promise": {
+ if (tokeniser.probe("["))
+ tokeniser.error("Promise type cannot have extended attribute");
+ const subtype =
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.return_type)(tokeniser, typeName) ||
+ tokeniser.error("Missing Promise subtype");
+ ret.subtype.push(subtype);
+ break;
+ }
+ case "sequence":
+ case "FrozenArray":
+ case "ObservableArray": {
+ const subtype =
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser, typeName) ||
+ tokeniser.error(`Missing ${base.value} subtype`);
+ ret.subtype.push(subtype);
+ break;
+ }
+ case "record": {
+ if (tokeniser.probe("["))
+ tokeniser.error("Record key cannot have extended attribute");
+ const keyType =
+ tokeniser.consume(..._tokeniser_js__WEBPACK_IMPORTED_MODULE_2__.stringTypes) ||
+ tokeniser.error(`Record key must be one of: ${_tokeniser_js__WEBPACK_IMPORTED_MODULE_2__.stringTypes.join(", ")}`);
+ const keyIdlType = new Type({
+ source: tokeniser.source,
+ tokens: { base: keyType },
+ });
+ keyIdlType.tokens.separator =
+ tokeniser.consume(",") ||
+ tokeniser.error("Missing comma after record key type");
+ keyIdlType.type = typeName;
+ const valueType =
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser, typeName) ||
+ tokeniser.error("Error parsing generic type record");
+ ret.subtype.push(keyIdlType, valueType);
+ break;
+ }
+ }
+ if (!ret.idlType) tokeniser.error(`Error parsing generic type ${base.value}`);
+ ret.tokens.close =
+ tokeniser.consume(">") ||
+ tokeniser.error(`Missing closing bracket after ${base.value}`);
+ return ret.this;
+}
+
+/**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+function type_suffix(tokeniser, obj) {
+ const nullable = tokeniser.consume("?");
+ if (nullable) {
+ obj.tokens.nullable = nullable;
+ }
+ if (tokeniser.probe("?")) tokeniser.error("Can't nullable more than once");
+}
+
+/**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {string} typeName
+ */
+function single_type(tokeniser, typeName) {
+ let ret = generic_type(tokeniser, typeName) || (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.primitive_type)(tokeniser);
+ if (!ret) {
+ const base =
+ tokeniser.consumeKind("identifier") ||
+ tokeniser.consume(..._tokeniser_js__WEBPACK_IMPORTED_MODULE_2__.stringTypes, ..._tokeniser_js__WEBPACK_IMPORTED_MODULE_2__.typeNameKeywords);
+ if (!base) {
+ return;
+ }
+ ret = new Type({ source: tokeniser.source, tokens: { base } });
+ if (tokeniser.probe("<"))
+ tokeniser.error(`Unsupported generic type ${base.value}`);
+ }
+ if (ret.generic === "Promise" && tokeniser.probe("?")) {
+ tokeniser.error("Promise type cannot be nullable");
+ }
+ ret.type = typeName || null;
+ type_suffix(tokeniser, ret);
+ if (ret.nullable && ret.idlType === "any")
+ tokeniser.error("Type `any` cannot be made nullable");
+ return ret;
+}
+
+/**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {string} type
+ */
+function union_type(tokeniser, type) {
+ const tokens = {};
+ tokens.open = tokeniser.consume("(");
+ if (!tokens.open) return;
+ const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)(new Type({ source: tokeniser.source, tokens }));
+ ret.type = type || null;
+ while (true) {
+ const typ =
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser) ||
+ tokeniser.error("No type after open parenthesis or 'or' in union type");
+ if (typ.idlType === "any")
+ tokeniser.error("Type `any` cannot be included in a union type");
+ if (typ.generic === "Promise")
+ tokeniser.error("Type `Promise` cannot be included in a union type");
+ ret.subtype.push(typ);
+ const or = tokeniser.consume("or");
+ if (or) {
+ typ.tokens.separator = or;
+ } else break;
+ }
+ if (ret.idlType.length < 2) {
+ tokeniser.error(
+ "At least two types are expected in a union type but found less"
+ );
+ }
+ tokens.close =
+ tokeniser.consume(")") || tokeniser.error("Unterminated union type");
+ type_suffix(tokeniser, ret);
+ return ret.this;
+}
+
+class Type extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {string} typeName
+ */
+ static parse(tokeniser, typeName) {
+ return single_type(tokeniser, typeName) || union_type(tokeniser, typeName);
+ }
+
+ constructor({ source, tokens }) {
+ super({ source, tokens });
+ Object.defineProperty(this, "subtype", { value: [], writable: true });
+ this.extAttrs = new _extended_attributes_js__WEBPACK_IMPORTED_MODULE_5__.ExtendedAttributes({ source, tokens: {} });
+ }
+
+ get generic() {
+ if (this.subtype.length && this.tokens.base) {
+ return this.tokens.base.value;
+ }
+ return "";
+ }
+ get nullable() {
+ return Boolean(this.tokens.nullable);
+ }
+ get union() {
+ return Boolean(this.subtype.length) && !this.tokens.base;
+ }
+ get idlType() {
+ if (this.subtype.length) {
+ return this.subtype;
+ }
+ // Adding prefixes/postfixes for "unrestricted float", etc.
+ const name = [this.tokens.prefix, this.tokens.base, this.tokens.postfix]
+ .filter((t) => t)
+ .map((t) => t.value)
+ .join(" ");
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(name);
+ }
+
+ *validate(defs) {
+ yield* this.extAttrs.validate(defs);
+
+ if (this.idlType === "void") {
+ const message = `\`void\` is now replaced by \`undefined\`. Refer to the \
+[relevant GitHub issue](https://github.com/whatwg/webidl/issues/60) \
+for more information.`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_3__.validationError)(this.tokens.base, this, "replace-void", message, {
+ autofix: replaceVoid(this),
+ });
+ }
+
+ /*
+ * If a union is nullable, its subunions cannot include a dictionary
+ * If not, subunions may include dictionaries if each union is not nullable
+ */
+ const typedef = !this.union && defs.unique.get(this.idlType);
+ const target = this.union
+ ? this
+ : typedef && typedef.type === "typedef"
+ ? typedef.idlType
+ : undefined;
+ if (target && this.nullable) {
+ // do not allow any dictionary
+ const { reference } = (0,_validators_helpers_js__WEBPACK_IMPORTED_MODULE_4__.idlTypeIncludesDictionary)(target, defs) || {};
+ if (reference) {
+ const targetToken = (this.union ? reference : this).tokens.base;
+ const message = "Nullable union cannot include a dictionary type.";
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_3__.validationError)(
+ targetToken,
+ this,
+ "no-nullable-union-dict",
+ message
+ );
+ }
+ } else {
+ // allow some dictionary
+ for (const subtype of this.subtype) {
+ yield* subtype.validate(defs);
+ }
+ }
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ const type_body = () => {
+ if (this.union || this.generic) {
+ return w.ts.wrap([
+ w.token(this.tokens.base, w.ts.generic),
+ w.token(this.tokens.open),
+ ...this.subtype.map((t) => t.write(w)),
+ w.token(this.tokens.close),
+ ]);
+ }
+ const firstToken = this.tokens.prefix || this.tokens.base;
+ const prefix = this.tokens.prefix
+ ? [this.tokens.prefix.value, w.ts.trivia(this.tokens.base.trivia)]
+ : [];
+ const ref = w.reference(
+ w.ts.wrap([
+ ...prefix,
+ this.tokens.base.value,
+ w.token(this.tokens.postfix),
+ ]),
+ {
+ unescaped: /** @type {string} (because it's not union) */ (
+ this.idlType
+ ),
+ context: this,
+ }
+ );
+ return w.ts.wrap([w.ts.trivia(firstToken.trivia), ref]);
+ };
+ return w.ts.wrap([
+ this.extAttrs.write(w),
+ type_body(),
+ w.token(this.tokens.nullable),
+ w.token(this.tokens.separator),
+ ]);
+ }
+}
+
+/**
+ * @param {Type} type
+ */
+function replaceVoid(type) {
+ return () => {
+ type.tokens.base.value = "undefined";
+ };
+}
+
+
+/***/ }),
+/* 6 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Base": () => (/* binding */ Base)
+/* harmony export */ });
+class Base {
+ /**
+ * @param {object} initializer
+ * @param {Base["source"]} initializer.source
+ * @param {Base["tokens"]} initializer.tokens
+ */
+ constructor({ source, tokens }) {
+ Object.defineProperties(this, {
+ source: { value: source },
+ tokens: { value: tokens, writable: true },
+ parent: { value: null, writable: true },
+ this: { value: this }, // useful when escaping from proxy
+ });
+ }
+
+ toJSON() {
+ const json = { type: undefined, name: undefined, inheritance: undefined };
+ let proto = this;
+ while (proto !== Object.prototype) {
+ const descMap = Object.getOwnPropertyDescriptors(proto);
+ for (const [key, value] of Object.entries(descMap)) {
+ if (value.enumerable || value.get) {
+ // @ts-ignore - allow indexing here
+ json[key] = this[key];
+ }
+ }
+ proto = Object.getPrototypeOf(proto);
+ }
+ return json;
+ }
+}
+
+
+/***/ }),
+/* 7 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "dictionaryIncludesRequiredField": () => (/* binding */ dictionaryIncludesRequiredField),
+/* harmony export */ "idlTypeIncludesDictionary": () => (/* binding */ idlTypeIncludesDictionary)
+/* harmony export */ });
+/**
+ * @typedef {import("../productions/dictionary.js").Dictionary} Dictionary
+ *
+ * @param {*} idlType
+ * @param {import("../validator.js").Definitions} defs
+ * @param {object} [options]
+ * @param {boolean} [options.useNullableInner] use when the input idlType is nullable and you want to use its inner type
+ * @return {{ reference: *, dictionary: Dictionary }} the type reference that ultimately includes dictionary.
+ */
+function idlTypeIncludesDictionary(
+ idlType,
+ defs,
+ { useNullableInner } = {}
+) {
+ if (!idlType.union) {
+ const def = defs.unique.get(idlType.idlType);
+ if (!def) {
+ return;
+ }
+ if (def.type === "typedef") {
+ const { typedefIncludesDictionary } = defs.cache;
+ if (typedefIncludesDictionary.has(def)) {
+ // Note that this also halts when it met indeterminate state
+ // to prevent infinite recursion
+ return typedefIncludesDictionary.get(def);
+ }
+ defs.cache.typedefIncludesDictionary.set(def, undefined); // indeterminate state
+ const result = idlTypeIncludesDictionary(def.idlType, defs);
+ defs.cache.typedefIncludesDictionary.set(def, result);
+ if (result) {
+ return {
+ reference: idlType,
+ dictionary: result.dictionary,
+ };
+ }
+ }
+ if (def.type === "dictionary" && (useNullableInner || !idlType.nullable)) {
+ return {
+ reference: idlType,
+ dictionary: def,
+ };
+ }
+ }
+ for (const subtype of idlType.subtype) {
+ const result = idlTypeIncludesDictionary(subtype, defs);
+ if (result) {
+ if (subtype.union) {
+ return result;
+ }
+ return {
+ reference: subtype,
+ dictionary: result.dictionary,
+ };
+ }
+ }
+}
+
+/**
+ * @param {*} dict dictionary type
+ * @param {import("../validator.js").Definitions} defs
+ * @return {boolean}
+ */
+function dictionaryIncludesRequiredField(dict, defs) {
+ if (defs.cache.dictionaryIncludesRequiredField.has(dict)) {
+ return defs.cache.dictionaryIncludesRequiredField.get(dict);
+ }
+ // Set cached result to indeterminate to short-circuit circular definitions.
+ // The final result will be updated to true or false.
+ defs.cache.dictionaryIncludesRequiredField.set(dict, undefined);
+ let result = dict.members.some((field) => field.required);
+ if (!result && dict.inheritance) {
+ const superdict = defs.unique.get(dict.inheritance);
+ if (!superdict) {
+ // Assume required members in the supertype if it is unknown.
+ result = true;
+ } else if (dictionaryIncludesRequiredField(superdict, defs)) {
+ result = true;
+ }
+ }
+ defs.cache.dictionaryIncludesRequiredField.set(dict, result);
+ return result;
+}
+
+
+/***/ }),
+/* 8 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "ExtendedAttributeParameters": () => (/* binding */ ExtendedAttributeParameters),
+/* harmony export */ "ExtendedAttributes": () => (/* binding */ ExtendedAttributes),
+/* harmony export */ "SimpleExtendedAttribute": () => (/* binding */ SimpleExtendedAttribute)
+/* harmony export */ });
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6);
+/* harmony import */ var _array_base_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(9);
+/* harmony import */ var _token_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(10);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(4);
+/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(3);
+
+
+
+
+
+
+/**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {string} tokenName
+ */
+function tokens(tokeniser, tokenName) {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.list)(tokeniser, {
+ parser: _token_js__WEBPACK_IMPORTED_MODULE_2__.WrappedToken.parser(tokeniser, tokenName),
+ listName: tokenName + " list",
+ });
+}
+
+const extAttrValueSyntax = ["identifier", "decimal", "integer", "string"];
+
+const shouldBeLegacyPrefixed = [
+ "NoInterfaceObject",
+ "LenientSetter",
+ "LenientThis",
+ "TreatNonObjectAsNull",
+ "Unforgeable",
+];
+
+const renamedLegacies = new Map([
+ .../** @type {[string, string][]} */ (
+ shouldBeLegacyPrefixed.map((name) => [name, `Legacy${name}`])
+ ),
+ ["NamedConstructor", "LegacyFactoryFunction"],
+ ["OverrideBuiltins", "LegacyOverrideBuiltIns"],
+ ["TreatNullAs", "LegacyNullToEmptyString"],
+]);
+
+/**
+ * This will allow a set of extended attribute values to be parsed.
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+function extAttrListItems(tokeniser) {
+ for (const syntax of extAttrValueSyntax) {
+ const toks = tokens(tokeniser, syntax);
+ if (toks.length) {
+ return toks;
+ }
+ }
+ tokeniser.error(
+ `Expected identifiers, strings, decimals, or integers but none found`
+ );
+}
+
+class ExtendedAttributeParameters extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser) {
+ const tokens = { assign: tokeniser.consume("=") };
+ const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.autoParenter)(
+ new ExtendedAttributeParameters({ source: tokeniser.source, tokens })
+ );
+ ret.list = [];
+ if (tokens.assign) {
+ tokens.asterisk = tokeniser.consume("*");
+ if (tokens.asterisk) {
+ return ret.this;
+ }
+ tokens.secondaryName = tokeniser.consumeKind(...extAttrValueSyntax);
+ }
+ tokens.open = tokeniser.consume("(");
+ if (tokens.open) {
+ ret.list = ret.rhsIsList
+ ? // [Exposed=(Window,Worker)]
+ extAttrListItems(tokeniser)
+ : // [LegacyFactoryFunction=Audio(DOMString src)] or [Constructor(DOMString str)]
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.argument_list)(tokeniser);
+ tokens.close =
+ tokeniser.consume(")") ||
+ tokeniser.error("Unexpected token in extended attribute argument list");
+ } else if (tokens.assign && !tokens.secondaryName) {
+ tokeniser.error("No right hand side to extended attribute assignment");
+ }
+ return ret.this;
+ }
+
+ get rhsIsList() {
+ return (
+ this.tokens.assign && !this.tokens.asterisk && !this.tokens.secondaryName
+ );
+ }
+
+ get rhsType() {
+ if (this.rhsIsList) {
+ return this.list[0].tokens.value.type + "-list";
+ }
+ if (this.tokens.asterisk) {
+ return "*";
+ }
+ if (this.tokens.secondaryName) {
+ return this.tokens.secondaryName.type;
+ }
+ return null;
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ const { rhsType } = this;
+ return w.ts.wrap([
+ w.token(this.tokens.assign),
+ w.token(this.tokens.asterisk),
+ w.reference_token(this.tokens.secondaryName, this.parent),
+ w.token(this.tokens.open),
+ ...this.list.map((p) => {
+ return rhsType === "identifier-list"
+ ? w.identifier(p, this.parent)
+ : p.write(w);
+ }),
+ w.token(this.tokens.close),
+ ]);
+ }
+}
+
+class SimpleExtendedAttribute extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser) {
+ const name = tokeniser.consumeKind("identifier");
+ if (name) {
+ return new SimpleExtendedAttribute({
+ source: tokeniser.source,
+ tokens: { name },
+ params: ExtendedAttributeParameters.parse(tokeniser),
+ });
+ }
+ }
+
+ constructor({ source, tokens, params }) {
+ super({ source, tokens });
+ params.parent = this;
+ Object.defineProperty(this, "params", { value: params });
+ }
+
+ get type() {
+ return "extended-attribute";
+ }
+ get name() {
+ return this.tokens.name.value;
+ }
+ get rhs() {
+ const { rhsType: type, tokens, list } = this.params;
+ if (!type) {
+ return null;
+ }
+ const value = this.params.rhsIsList
+ ? list
+ : this.params.tokens.secondaryName
+ ? (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.unescape)(tokens.secondaryName.value)
+ : null;
+ return { type, value };
+ }
+ get arguments() {
+ const { rhsIsList, list } = this.params;
+ if (!list || rhsIsList) {
+ return [];
+ }
+ return list;
+ }
+
+ *validate(defs) {
+ const { name } = this;
+ if (name === "LegacyNoInterfaceObject") {
+ const message = `\`[LegacyNoInterfaceObject]\` extended attribute is an \
+undesirable feature that may be removed from Web IDL in the future. Refer to the \
+[relevant upstream PR](https://github.com/whatwg/webidl/pull/609) for more \
+information.`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_4__.validationError)(
+ this.tokens.name,
+ this,
+ "no-nointerfaceobject",
+ message,
+ { level: "warning" }
+ );
+ } else if (renamedLegacies.has(name)) {
+ const message = `\`[${name}]\` extended attribute is a legacy feature \
+that is now renamed to \`[${renamedLegacies.get(name)}]\`. Refer to the \
+[relevant upstream PR](https://github.com/whatwg/webidl/pull/870) for more \
+information.`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_4__.validationError)(this.tokens.name, this, "renamed-legacy", message, {
+ level: "warning",
+ autofix: renameLegacyExtendedAttribute(this),
+ });
+ }
+ for (const arg of this.arguments) {
+ yield* arg.validate(defs);
+ }
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ return w.ts.wrap([
+ w.ts.trivia(this.tokens.name.trivia),
+ w.ts.extendedAttribute(
+ w.ts.wrap([
+ w.ts.extendedAttributeReference(this.name),
+ this.params.write(w),
+ ])
+ ),
+ w.token(this.tokens.separator),
+ ]);
+ }
+}
+
+/**
+ * @param {SimpleExtendedAttribute} extAttr
+ */
+function renameLegacyExtendedAttribute(extAttr) {
+ return () => {
+ const { name } = extAttr;
+ extAttr.tokens.name.value = renamedLegacies.get(name);
+ if (name === "TreatNullAs") {
+ extAttr.params.tokens = {};
+ }
+ };
+}
+
+// Note: we parse something simpler than the official syntax. It's all that ever
+// seems to be used
+class ExtendedAttributes extends _array_base_js__WEBPACK_IMPORTED_MODULE_1__.ArrayBase {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser) {
+ const tokens = {};
+ tokens.open = tokeniser.consume("[");
+ const ret = new ExtendedAttributes({ source: tokeniser.source, tokens });
+ if (!tokens.open) return ret;
+ ret.push(
+ ...(0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.list)(tokeniser, {
+ parser: SimpleExtendedAttribute.parse,
+ listName: "extended attribute",
+ })
+ );
+ tokens.close =
+ tokeniser.consume("]") ||
+ tokeniser.error(
+ "Expected a closing token for the extended attribute list"
+ );
+ if (!ret.length) {
+ tokeniser.unconsume(tokens.close.index);
+ tokeniser.error("An extended attribute list must not be empty");
+ }
+ if (tokeniser.probe("[")) {
+ tokeniser.error(
+ "Illegal double extended attribute lists, consider merging them"
+ );
+ }
+ return ret;
+ }
+
+ *validate(defs) {
+ for (const extAttr of this) {
+ yield* extAttr.validate(defs);
+ }
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ if (!this.length) return "";
+ return w.ts.wrap([
+ w.token(this.tokens.open),
+ ...this.map((ea) => ea.write(w)),
+ w.token(this.tokens.close),
+ ]);
+ }
+}
+
+
+/***/ }),
+/* 9 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "ArrayBase": () => (/* binding */ ArrayBase)
+/* harmony export */ });
+class ArrayBase extends Array {
+ constructor({ source, tokens }) {
+ super();
+ Object.defineProperties(this, {
+ source: { value: source },
+ tokens: { value: tokens },
+ parent: { value: null, writable: true },
+ });
+ }
+}
+
+
+/***/ }),
+/* 10 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Eof": () => (/* binding */ Eof),
+/* harmony export */ "WrappedToken": () => (/* binding */ WrappedToken)
+/* harmony export */ });
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
+
+
+
+class WrappedToken extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {string} type
+ */
+ static parser(tokeniser, type) {
+ return () => {
+ const value = tokeniser.consumeKind(type);
+ if (value) {
+ return new WrappedToken({
+ source: tokeniser.source,
+ tokens: { value },
+ });
+ }
+ };
+ }
+
+ get value() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(this.tokens.value.value);
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ return w.ts.wrap([
+ w.token(this.tokens.value),
+ w.token(this.tokens.separator),
+ ]);
+ }
+}
+
+class Eof extends WrappedToken {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser) {
+ const value = tokeniser.consumeKind("eof");
+ if (value) {
+ return new Eof({ source: tokeniser.source, tokens: { value } });
+ }
+ }
+
+ get type() {
+ return "eof";
+ }
+}
+
+
+/***/ }),
+/* 11 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Argument": () => (/* binding */ Argument)
+/* harmony export */ });
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6);
+/* harmony import */ var _default_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(12);
+/* harmony import */ var _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(8);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(4);
+/* harmony import */ var _tokeniser_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(2);
+/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(3);
+/* harmony import */ var _validators_helpers_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(7);
+
+
+
+
+
+
+
+
+class Argument extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser) {
+ const start_position = tokeniser.position;
+ /** @type {Base["tokens"]} */
+ const tokens = {};
+ const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.autoParenter)(
+ new Argument({ source: tokeniser.source, tokens })
+ );
+ ret.extAttrs = _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__.ExtendedAttributes.parse(tokeniser);
+ tokens.optional = tokeniser.consume("optional");
+ ret.idlType = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.type_with_extended_attributes)(tokeniser, "argument-type");
+ if (!ret.idlType) {
+ return tokeniser.unconsume(start_position);
+ }
+ if (!tokens.optional) {
+ tokens.variadic = tokeniser.consume("...");
+ }
+ tokens.name =
+ tokeniser.consumeKind("identifier") ||
+ tokeniser.consume(..._tokeniser_js__WEBPACK_IMPORTED_MODULE_4__.argumentNameKeywords);
+ if (!tokens.name) {
+ return tokeniser.unconsume(start_position);
+ }
+ ret.default = tokens.optional ? _default_js__WEBPACK_IMPORTED_MODULE_1__.Default.parse(tokeniser) : null;
+ return ret.this;
+ }
+
+ get type() {
+ return "argument";
+ }
+ get optional() {
+ return !!this.tokens.optional;
+ }
+ get variadic() {
+ return !!this.tokens.variadic;
+ }
+ get name() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.unescape)(this.tokens.name.value);
+ }
+
+ /**
+ * @param {import("../validator.js").Definitions} defs
+ */
+ *validate(defs) {
+ yield* this.extAttrs.validate(defs);
+ yield* this.idlType.validate(defs);
+ const result = (0,_validators_helpers_js__WEBPACK_IMPORTED_MODULE_6__.idlTypeIncludesDictionary)(this.idlType, defs, {
+ useNullableInner: true,
+ });
+ if (result) {
+ if (this.idlType.nullable) {
+ const message = `Dictionary arguments cannot be nullable.`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_5__.validationError)(
+ this.tokens.name,
+ this,
+ "no-nullable-dict-arg",
+ message
+ );
+ } else if (!this.optional) {
+ if (
+ this.parent &&
+ !(0,_validators_helpers_js__WEBPACK_IMPORTED_MODULE_6__.dictionaryIncludesRequiredField)(result.dictionary, defs) &&
+ isLastRequiredArgument(this)
+ ) {
+ const message = `Dictionary argument must be optional if it has no required fields`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_5__.validationError)(
+ this.tokens.name,
+ this,
+ "dict-arg-optional",
+ message,
+ {
+ autofix: autofixDictionaryArgumentOptionality(this),
+ }
+ );
+ }
+ } else if (!this.default) {
+ const message = `Optional dictionary arguments must have a default value of \`{}\`.`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_5__.validationError)(
+ this.tokens.name,
+ this,
+ "dict-arg-default",
+ message,
+ {
+ autofix: autofixOptionalDictionaryDefaultValue(this),
+ }
+ );
+ }
+ }
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ return w.ts.wrap([
+ this.extAttrs.write(w),
+ w.token(this.tokens.optional),
+ w.ts.type(this.idlType.write(w)),
+ w.token(this.tokens.variadic),
+ w.name_token(this.tokens.name, { data: this }),
+ this.default ? this.default.write(w) : "",
+ w.token(this.tokens.separator),
+ ]);
+ }
+}
+
+/**
+ * @param {Argument} arg
+ */
+function isLastRequiredArgument(arg) {
+ const list = arg.parent.arguments || arg.parent.list;
+ const index = list.indexOf(arg);
+ const requiredExists = list.slice(index + 1).some((a) => !a.optional);
+ return !requiredExists;
+}
+
+/**
+ * @param {Argument} arg
+ */
+function autofixDictionaryArgumentOptionality(arg) {
+ return () => {
+ const firstToken = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.getFirstToken)(arg.idlType);
+ arg.tokens.optional = {
+ ...firstToken,
+ type: "optional",
+ value: "optional",
+ };
+ firstToken.trivia = " ";
+ autofixOptionalDictionaryDefaultValue(arg)();
+ };
+}
+
+/**
+ * @param {Argument} arg
+ */
+function autofixOptionalDictionaryDefaultValue(arg) {
+ return () => {
+ arg.default = _default_js__WEBPACK_IMPORTED_MODULE_1__.Default.parse(new _tokeniser_js__WEBPACK_IMPORTED_MODULE_4__.Tokeniser(" = {}"));
+ };
+}
+
+
+/***/ }),
+/* 12 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Default": () => (/* binding */ Default)
+/* harmony export */ });
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
+
+
+
+class Default extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser) {
+ const assign = tokeniser.consume("=");
+ if (!assign) {
+ return null;
+ }
+ const def =
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.const_value)(tokeniser) ||
+ tokeniser.consumeKind("string") ||
+ tokeniser.consume("null", "[", "{") ||
+ tokeniser.error("No value for default");
+ const expression = [def];
+ if (def.value === "[") {
+ const close =
+ tokeniser.consume("]") ||
+ tokeniser.error("Default sequence value must be empty");
+ expression.push(close);
+ } else if (def.value === "{") {
+ const close =
+ tokeniser.consume("}") ||
+ tokeniser.error("Default dictionary value must be empty");
+ expression.push(close);
+ }
+ return new Default({
+ source: tokeniser.source,
+ tokens: { assign },
+ expression,
+ });
+ }
+
+ constructor({ source, tokens, expression }) {
+ super({ source, tokens });
+ expression.parent = this;
+ Object.defineProperty(this, "expression", { value: expression });
+ }
+
+ get type() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.const_data)(this.expression[0]).type;
+ }
+ get value() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.const_data)(this.expression[0]).value;
+ }
+ get negative() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.const_data)(this.expression[0]).negative;
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ return w.ts.wrap([
+ w.token(this.tokens.assign),
+ ...this.expression.map((t) => w.token(t)),
+ ]);
+ }
+}
+
+
+/***/ }),
+/* 13 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Operation": () => (/* binding */ Operation)
+/* harmony export */ });
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
+/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(3);
+
+
+
+
+class Operation extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @typedef {import("../tokeniser.js").Token} Token
+ *
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {object} [options]
+ * @param {Token} [options.special]
+ * @param {Token} [options.regular]
+ */
+ static parse(tokeniser, { special, regular } = {}) {
+ const tokens = { special };
+ const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)(
+ new Operation({ source: tokeniser.source, tokens })
+ );
+ if (special && special.value === "stringifier") {
+ tokens.termination = tokeniser.consume(";");
+ if (tokens.termination) {
+ ret.arguments = [];
+ return ret;
+ }
+ }
+ if (!special && !regular) {
+ tokens.special = tokeniser.consume("getter", "setter", "deleter");
+ }
+ ret.idlType =
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.return_type)(tokeniser) || tokeniser.error("Missing return type");
+ tokens.name =
+ tokeniser.consumeKind("identifier") || tokeniser.consume("includes");
+ tokens.open =
+ tokeniser.consume("(") || tokeniser.error("Invalid operation");
+ ret.arguments = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.argument_list)(tokeniser);
+ tokens.close =
+ tokeniser.consume(")") || tokeniser.error("Unterminated operation");
+ tokens.termination =
+ tokeniser.consume(";") ||
+ tokeniser.error("Unterminated operation, expected `;`");
+ return ret.this;
+ }
+
+ get type() {
+ return "operation";
+ }
+ get name() {
+ const { name } = this.tokens;
+ if (!name) {
+ return "";
+ }
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(name.value);
+ }
+ get special() {
+ if (!this.tokens.special) {
+ return "";
+ }
+ return this.tokens.special.value;
+ }
+
+ *validate(defs) {
+ yield* this.extAttrs.validate(defs);
+ if (!this.name && ["", "static"].includes(this.special)) {
+ const message = `Regular or static operations must have both a return type and an identifier.`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_2__.validationError)(this.tokens.open, this, "incomplete-op", message);
+ }
+ if (this.idlType) {
+ yield* this.idlType.validate(defs);
+ }
+ for (const argument of this.arguments) {
+ yield* argument.validate(defs);
+ }
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ const { parent } = this;
+ const body = this.idlType
+ ? [
+ w.ts.type(this.idlType.write(w)),
+ w.name_token(this.tokens.name, { data: this, parent }),
+ w.token(this.tokens.open),
+ w.ts.wrap(this.arguments.map((arg) => arg.write(w))),
+ w.token(this.tokens.close),
+ ]
+ : [];
+ return w.ts.definition(
+ w.ts.wrap([
+ this.extAttrs.write(w),
+ this.tokens.name
+ ? w.token(this.tokens.special)
+ : w.token(this.tokens.special, w.ts.nameless, { data: this, parent }),
+ ...body,
+ w.token(this.tokens.termination),
+ ]),
+ { data: this, parent }
+ );
+ }
+}
+
+
+/***/ }),
+/* 14 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Attribute": () => (/* binding */ Attribute)
+/* harmony export */ });
+/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3);
+/* harmony import */ var _validators_helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(7);
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(4);
+
+
+
+
+
+class Attribute extends _base_js__WEBPACK_IMPORTED_MODULE_2__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {object} [options]
+ * @param {import("../tokeniser.js").Token} [options.special]
+ * @param {boolean} [options.noInherit]
+ * @param {boolean} [options.readonly]
+ */
+ static parse(
+ tokeniser,
+ { special, noInherit = false, readonly = false } = {}
+ ) {
+ const start_position = tokeniser.position;
+ const tokens = { special };
+ const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.autoParenter)(
+ new Attribute({ source: tokeniser.source, tokens })
+ );
+ if (!special && !noInherit) {
+ tokens.special = tokeniser.consume("inherit");
+ }
+ if (ret.special === "inherit" && tokeniser.probe("readonly")) {
+ tokeniser.error("Inherited attributes cannot be read-only");
+ }
+ tokens.readonly = tokeniser.consume("readonly");
+ if (readonly && !tokens.readonly && tokeniser.probe("attribute")) {
+ tokeniser.error("Attributes must be readonly in this context");
+ }
+ tokens.base = tokeniser.consume("attribute");
+ if (!tokens.base) {
+ tokeniser.unconsume(start_position);
+ return;
+ }
+ ret.idlType =
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.type_with_extended_attributes)(tokeniser, "attribute-type") ||
+ tokeniser.error("Attribute lacks a type");
+ tokens.name =
+ tokeniser.consumeKind("identifier") ||
+ tokeniser.consume("async", "required") ||
+ tokeniser.error("Attribute lacks a name");
+ tokens.termination =
+ tokeniser.consume(";") ||
+ tokeniser.error("Unterminated attribute, expected `;`");
+ return ret.this;
+ }
+
+ get type() {
+ return "attribute";
+ }
+ get special() {
+ if (!this.tokens.special) {
+ return "";
+ }
+ return this.tokens.special.value;
+ }
+ get readonly() {
+ return !!this.tokens.readonly;
+ }
+ get name() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.unescape)(this.tokens.name.value);
+ }
+
+ *validate(defs) {
+ yield* this.extAttrs.validate(defs);
+ yield* this.idlType.validate(defs);
+
+ switch (this.idlType.generic) {
+ case "sequence":
+ case "record": {
+ const message = `Attributes cannot accept ${this.idlType.generic} types.`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.validationError)(
+ this.tokens.name,
+ this,
+ "attr-invalid-type",
+ message
+ );
+ break;
+ }
+ default: {
+ const { reference } =
+ (0,_validators_helpers_js__WEBPACK_IMPORTED_MODULE_1__.idlTypeIncludesDictionary)(this.idlType, defs) || {};
+ if (reference) {
+ const targetToken = (this.idlType.union ? reference : this.idlType)
+ .tokens.base;
+ const message = "Attributes cannot accept dictionary types.";
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.validationError)(
+ targetToken,
+ this,
+ "attr-invalid-type",
+ message
+ );
+ }
+ }
+ }
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ const { parent } = this;
+ return w.ts.definition(
+ w.ts.wrap([
+ this.extAttrs.write(w),
+ w.token(this.tokens.special),
+ w.token(this.tokens.readonly),
+ w.token(this.tokens.base),
+ w.ts.type(this.idlType.write(w)),
+ w.name_token(this.tokens.name, { data: this, parent }),
+ w.token(this.tokens.termination),
+ ]),
+ { data: this, parent }
+ );
+ }
+}
+
+
+/***/ }),
+/* 15 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Enum": () => (/* binding */ Enum),
+/* harmony export */ "EnumValue": () => (/* binding */ EnumValue)
+/* harmony export */ });
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4);
+/* harmony import */ var _token_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(10);
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6);
+
+
+
+
+class EnumValue extends _token_js__WEBPACK_IMPORTED_MODULE_1__.WrappedToken {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser) {
+ const value = tokeniser.consumeKind("string");
+ if (value) {
+ return new EnumValue({ source: tokeniser.source, tokens: { value } });
+ }
+ }
+
+ get type() {
+ return "enum-value";
+ }
+ get value() {
+ return super.value.slice(1, -1);
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ const { parent } = this;
+ return w.ts.wrap([
+ w.ts.trivia(this.tokens.value.trivia),
+ w.ts.definition(
+ w.ts.wrap(['"', w.ts.name(this.value, { data: this, parent }), '"']),
+ { data: this, parent }
+ ),
+ w.token(this.tokens.separator),
+ ]);
+ }
+}
+
+class Enum extends _base_js__WEBPACK_IMPORTED_MODULE_2__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser) {
+ /** @type {Base["tokens"]} */
+ const tokens = {};
+ tokens.base = tokeniser.consume("enum");
+ if (!tokens.base) {
+ return;
+ }
+ tokens.name =
+ tokeniser.consumeKind("identifier") ||
+ tokeniser.error("No name for enum");
+ const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.autoParenter)(new Enum({ source: tokeniser.source, tokens }));
+ tokeniser.current = ret.this;
+ tokens.open = tokeniser.consume("{") || tokeniser.error("Bodyless enum");
+ ret.values = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.list)(tokeniser, {
+ parser: EnumValue.parse,
+ allowDangler: true,
+ listName: "enumeration",
+ });
+ if (tokeniser.probeKind("string")) {
+ tokeniser.error("No comma between enum values");
+ }
+ tokens.close =
+ tokeniser.consume("}") || tokeniser.error("Unexpected value in enum");
+ if (!ret.values.length) {
+ tokeniser.error("No value in enum");
+ }
+ tokens.termination =
+ tokeniser.consume(";") || tokeniser.error("No semicolon after enum");
+ return ret.this;
+ }
+
+ get type() {
+ return "enum";
+ }
+ get name() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.unescape)(this.tokens.name.value);
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ return w.ts.definition(
+ w.ts.wrap([
+ this.extAttrs.write(w),
+ w.token(this.tokens.base),
+ w.name_token(this.tokens.name, { data: this }),
+ w.token(this.tokens.open),
+ w.ts.wrap(this.values.map((v) => v.write(w))),
+ w.token(this.tokens.close),
+ w.token(this.tokens.termination),
+ ]),
+ { data: this }
+ );
+ }
+}
+
+
+/***/ }),
+/* 16 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Includes": () => (/* binding */ Includes)
+/* harmony export */ });
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
+
+
+
+class Includes extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser) {
+ const target = tokeniser.consumeKind("identifier");
+ if (!target) {
+ return;
+ }
+ const tokens = { target };
+ tokens.includes = tokeniser.consume("includes");
+ if (!tokens.includes) {
+ tokeniser.unconsume(target.index);
+ return;
+ }
+ tokens.mixin =
+ tokeniser.consumeKind("identifier") ||
+ tokeniser.error("Incomplete includes statement");
+ tokens.termination =
+ tokeniser.consume(";") ||
+ tokeniser.error("No terminating ; for includes statement");
+ return new Includes({ source: tokeniser.source, tokens });
+ }
+
+ get type() {
+ return "includes";
+ }
+ get target() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(this.tokens.target.value);
+ }
+ get includes() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(this.tokens.mixin.value);
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ return w.ts.definition(
+ w.ts.wrap([
+ this.extAttrs.write(w),
+ w.reference_token(this.tokens.target, this),
+ w.token(this.tokens.includes),
+ w.reference_token(this.tokens.mixin, this),
+ w.token(this.tokens.termination),
+ ]),
+ { data: this }
+ );
+ }
+}
+
+
+/***/ }),
+/* 17 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Typedef": () => (/* binding */ Typedef)
+/* harmony export */ });
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
+
+
+
+class Typedef extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser) {
+ /** @type {Base["tokens"]} */
+ const tokens = {};
+ const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)(new Typedef({ source: tokeniser.source, tokens }));
+ tokens.base = tokeniser.consume("typedef");
+ if (!tokens.base) {
+ return;
+ }
+ ret.idlType =
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser, "typedef-type") ||
+ tokeniser.error("Typedef lacks a type");
+ tokens.name =
+ tokeniser.consumeKind("identifier") ||
+ tokeniser.error("Typedef lacks a name");
+ tokeniser.current = ret.this;
+ tokens.termination =
+ tokeniser.consume(";") ||
+ tokeniser.error("Unterminated typedef, expected `;`");
+ return ret.this;
+ }
+
+ get type() {
+ return "typedef";
+ }
+ get name() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(this.tokens.name.value);
+ }
+
+ *validate(defs) {
+ yield* this.idlType.validate(defs);
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ return w.ts.definition(
+ w.ts.wrap([
+ this.extAttrs.write(w),
+ w.token(this.tokens.base),
+ w.ts.type(this.idlType.write(w)),
+ w.name_token(this.tokens.name, { data: this }),
+ w.token(this.tokens.termination),
+ ]),
+ { data: this }
+ );
+ }
+}
+
+
+/***/ }),
+/* 18 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "CallbackFunction": () => (/* binding */ CallbackFunction)
+/* harmony export */ });
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
+
+
+
+class CallbackFunction extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser, base) {
+ const tokens = { base };
+ const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)(
+ new CallbackFunction({ source: tokeniser.source, tokens })
+ );
+ tokens.name =
+ tokeniser.consumeKind("identifier") ||
+ tokeniser.error("Callback lacks a name");
+ tokeniser.current = ret.this;
+ tokens.assign =
+ tokeniser.consume("=") || tokeniser.error("Callback lacks an assignment");
+ ret.idlType =
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.return_type)(tokeniser) || tokeniser.error("Callback lacks a return type");
+ tokens.open =
+ tokeniser.consume("(") ||
+ tokeniser.error("Callback lacks parentheses for arguments");
+ ret.arguments = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.argument_list)(tokeniser);
+ tokens.close =
+ tokeniser.consume(")") || tokeniser.error("Unterminated callback");
+ tokens.termination =
+ tokeniser.consume(";") ||
+ tokeniser.error("Unterminated callback, expected `;`");
+ return ret.this;
+ }
+
+ get type() {
+ return "callback";
+ }
+ get name() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(this.tokens.name.value);
+ }
+
+ *validate(defs) {
+ yield* this.extAttrs.validate(defs);
+ yield* this.idlType.validate(defs);
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ return w.ts.definition(
+ w.ts.wrap([
+ this.extAttrs.write(w),
+ w.token(this.tokens.base),
+ w.name_token(this.tokens.name, { data: this }),
+ w.token(this.tokens.assign),
+ w.ts.type(this.idlType.write(w)),
+ w.token(this.tokens.open),
+ ...this.arguments.map((arg) => arg.write(w)),
+ w.token(this.tokens.close),
+ w.token(this.tokens.termination),
+ ]),
+ { data: this }
+ );
+ }
+}
+
+
+/***/ }),
+/* 19 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Interface": () => (/* binding */ Interface)
+/* harmony export */ });
+/* harmony import */ var _container_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(20);
+/* harmony import */ var _attribute_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(14);
+/* harmony import */ var _operation_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(13);
+/* harmony import */ var _constant_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(21);
+/* harmony import */ var _iterable_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(22);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(4);
+/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(3);
+/* harmony import */ var _validators_interface_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(23);
+/* harmony import */ var _constructor_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(24);
+/* harmony import */ var _tokeniser_js__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(2);
+/* harmony import */ var _extended_attributes_js__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(8);
+
+
+
+
+
+
+
+
+
+
+
+
+/**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+function static_member(tokeniser) {
+ const special = tokeniser.consume("static");
+ if (!special) return;
+ const member =
+ _attribute_js__WEBPACK_IMPORTED_MODULE_1__.Attribute.parse(tokeniser, { special }) ||
+ _operation_js__WEBPACK_IMPORTED_MODULE_2__.Operation.parse(tokeniser, { special }) ||
+ tokeniser.error("No body in static member");
+ return member;
+}
+
+class Interface extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser, base, { partial = null } = {}) {
+ const tokens = { partial, base };
+ return _container_js__WEBPACK_IMPORTED_MODULE_0__.Container.parse(
+ tokeniser,
+ new Interface({ source: tokeniser.source, tokens }),
+ {
+ inheritable: !partial,
+ allowedMembers: [
+ [_constant_js__WEBPACK_IMPORTED_MODULE_3__.Constant.parse],
+ [_constructor_js__WEBPACK_IMPORTED_MODULE_8__.Constructor.parse],
+ [static_member],
+ [_helpers_js__WEBPACK_IMPORTED_MODULE_5__.stringifier],
+ [_iterable_js__WEBPACK_IMPORTED_MODULE_4__.IterableLike.parse],
+ [_attribute_js__WEBPACK_IMPORTED_MODULE_1__.Attribute.parse],
+ [_operation_js__WEBPACK_IMPORTED_MODULE_2__.Operation.parse],
+ ],
+ }
+ );
+ }
+
+ get type() {
+ return "interface";
+ }
+
+ *validate(defs) {
+ yield* this.extAttrs.validate(defs);
+ if (
+ !this.partial &&
+ this.extAttrs.every((extAttr) => extAttr.name !== "Exposed")
+ ) {
+ const message = `Interfaces must have \`[Exposed]\` extended attribute. \
+To fix, add, for example, \`[Exposed=Window]\`. Please also consider carefully \
+if your interface should also be exposed in a Worker scope. Refer to the \
+[WebIDL spec section on Exposed](https://heycam.github.io/webidl/#Exposed) \
+for more information.`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_6__.validationError)(
+ this.tokens.name,
+ this,
+ "require-exposed",
+ message,
+ {
+ autofix: (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.autofixAddExposedWindow)(this),
+ }
+ );
+ }
+ const oldConstructors = this.extAttrs.filter(
+ (extAttr) => extAttr.name === "Constructor"
+ );
+ for (const constructor of oldConstructors) {
+ const message = `Constructors should now be represented as a \`constructor()\` operation on the interface \
+instead of \`[Constructor]\` extended attribute. Refer to the \
+[WebIDL spec section on constructor operations](https://heycam.github.io/webidl/#idl-constructors) \
+for more information.`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_6__.validationError)(
+ constructor.tokens.name,
+ this,
+ "constructor-member",
+ message,
+ {
+ autofix: autofixConstructor(this, constructor),
+ }
+ );
+ }
+
+ const isGlobal = this.extAttrs.some((extAttr) => extAttr.name === "Global");
+ if (isGlobal) {
+ const factoryFunctions = this.extAttrs.filter(
+ (extAttr) => extAttr.name === "LegacyFactoryFunction"
+ );
+ for (const named of factoryFunctions) {
+ const message = `Interfaces marked as \`[Global]\` cannot have factory functions.`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_6__.validationError)(
+ named.tokens.name,
+ this,
+ "no-constructible-global",
+ message
+ );
+ }
+
+ const constructors = this.members.filter(
+ (member) => member.type === "constructor"
+ );
+ for (const named of constructors) {
+ const message = `Interfaces marked as \`[Global]\` cannot have constructors.`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_6__.validationError)(
+ named.tokens.base,
+ this,
+ "no-constructible-global",
+ message
+ );
+ }
+ }
+
+ yield* super.validate(defs);
+ if (!this.partial) {
+ yield* (0,_validators_interface_js__WEBPACK_IMPORTED_MODULE_7__.checkInterfaceMemberDuplication)(defs, this);
+ }
+ }
+}
+
+function autofixConstructor(interfaceDef, constructorExtAttr) {
+ interfaceDef = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.autoParenter)(interfaceDef);
+ return () => {
+ const indentation = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.getLastIndentation)(
+ interfaceDef.extAttrs.tokens.open.trivia
+ );
+ const memberIndent = interfaceDef.members.length
+ ? (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.getLastIndentation)((0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.getFirstToken)(interfaceDef.members[0]).trivia)
+ : (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.getMemberIndentation)(indentation);
+ const constructorOp = _constructor_js__WEBPACK_IMPORTED_MODULE_8__.Constructor.parse(
+ new _tokeniser_js__WEBPACK_IMPORTED_MODULE_9__.Tokeniser(`\n${memberIndent}constructor();`)
+ );
+ constructorOp.extAttrs = new _extended_attributes_js__WEBPACK_IMPORTED_MODULE_10__.ExtendedAttributes({
+ source: interfaceDef.source,
+ tokens: {},
+ });
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.autoParenter)(constructorOp).arguments = constructorExtAttr.arguments;
+
+ const existingIndex = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.findLastIndex)(
+ interfaceDef.members,
+ (m) => m.type === "constructor"
+ );
+ interfaceDef.members.splice(existingIndex + 1, 0, constructorOp);
+
+ const { close } = interfaceDef.tokens;
+ if (!close.trivia.includes("\n")) {
+ close.trivia += `\n${indentation}`;
+ }
+
+ const { extAttrs } = interfaceDef;
+ const index = extAttrs.indexOf(constructorExtAttr);
+ const removed = extAttrs.splice(index, 1);
+ if (!extAttrs.length) {
+ extAttrs.tokens.open = extAttrs.tokens.close = undefined;
+ } else if (extAttrs.length === index) {
+ extAttrs[index - 1].tokens.separator = undefined;
+ } else if (!extAttrs[index].tokens.name.trivia.trim()) {
+ extAttrs[index].tokens.name.trivia = removed[0].tokens.name.trivia;
+ }
+ };
+}
+
+
+/***/ }),
+/* 20 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Container": () => (/* binding */ Container)
+/* harmony export */ });
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6);
+/* harmony import */ var _extended_attributes_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(8);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4);
+
+
+
+
+/**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+function inheritance(tokeniser) {
+ const colon = tokeniser.consume(":");
+ if (!colon) {
+ return {};
+ }
+ const inheritance =
+ tokeniser.consumeKind("identifier") ||
+ tokeniser.error("Inheritance lacks a type");
+ return { colon, inheritance };
+}
+
+class Container extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {*} instance TODO: This should be {T extends Container}, but see https://github.com/microsoft/TypeScript/issues/4628
+ * @param {*} args
+ */
+ static parse(tokeniser, instance, { inheritable, allowedMembers }) {
+ const { tokens, type } = instance;
+ tokens.name =
+ tokeniser.consumeKind("identifier") ||
+ tokeniser.error(`Missing name in ${type}`);
+ tokeniser.current = instance;
+ instance = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.autoParenter)(instance);
+ if (inheritable) {
+ Object.assign(tokens, inheritance(tokeniser));
+ }
+ tokens.open = tokeniser.consume("{") || tokeniser.error(`Bodyless ${type}`);
+ instance.members = [];
+ while (true) {
+ tokens.close = tokeniser.consume("}");
+ if (tokens.close) {
+ tokens.termination =
+ tokeniser.consume(";") ||
+ tokeniser.error(`Missing semicolon after ${type}`);
+ return instance.this;
+ }
+ const ea = _extended_attributes_js__WEBPACK_IMPORTED_MODULE_1__.ExtendedAttributes.parse(tokeniser);
+ let mem;
+ for (const [parser, ...args] of allowedMembers) {
+ mem = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.autoParenter)(parser(tokeniser, ...args));
+ if (mem) {
+ break;
+ }
+ }
+ if (!mem) {
+ tokeniser.error("Unknown member");
+ }
+ mem.extAttrs = ea;
+ instance.members.push(mem.this);
+ }
+ }
+
+ get partial() {
+ return !!this.tokens.partial;
+ }
+ get name() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.unescape)(this.tokens.name.value);
+ }
+ get inheritance() {
+ if (!this.tokens.inheritance) {
+ return null;
+ }
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.unescape)(this.tokens.inheritance.value);
+ }
+
+ *validate(defs) {
+ for (const member of this.members) {
+ if (member.validate) {
+ yield* member.validate(defs);
+ }
+ }
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ const inheritance = () => {
+ if (!this.tokens.inheritance) {
+ return "";
+ }
+ return w.ts.wrap([
+ w.token(this.tokens.colon),
+ w.ts.trivia(this.tokens.inheritance.trivia),
+ w.ts.inheritance(
+ w.reference(this.tokens.inheritance.value, { context: this })
+ ),
+ ]);
+ };
+
+ return w.ts.definition(
+ w.ts.wrap([
+ this.extAttrs.write(w),
+ w.token(this.tokens.callback),
+ w.token(this.tokens.partial),
+ w.token(this.tokens.base),
+ w.token(this.tokens.mixin),
+ w.name_token(this.tokens.name, { data: this }),
+ inheritance(),
+ w.token(this.tokens.open),
+ w.ts.wrap(this.members.map((m) => m.write(w))),
+ w.token(this.tokens.close),
+ w.token(this.tokens.termination),
+ ]),
+ { data: this }
+ );
+ }
+}
+
+
+/***/ }),
+/* 21 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Constant": () => (/* binding */ Constant)
+/* harmony export */ });
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6);
+/* harmony import */ var _type_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(5);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4);
+
+
+
+
+class Constant extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser) {
+ /** @type {Base["tokens"]} */
+ const tokens = {};
+ tokens.base = tokeniser.consume("const");
+ if (!tokens.base) {
+ return;
+ }
+ let idlType = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.primitive_type)(tokeniser);
+ if (!idlType) {
+ const base =
+ tokeniser.consumeKind("identifier") ||
+ tokeniser.error("Const lacks a type");
+ idlType = new _type_js__WEBPACK_IMPORTED_MODULE_1__.Type({ source: tokeniser.source, tokens: { base } });
+ }
+ if (tokeniser.probe("?")) {
+ tokeniser.error("Unexpected nullable constant type");
+ }
+ idlType.type = "const-type";
+ tokens.name =
+ tokeniser.consumeKind("identifier") ||
+ tokeniser.error("Const lacks a name");
+ tokens.assign =
+ tokeniser.consume("=") || tokeniser.error("Const lacks value assignment");
+ tokens.value =
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.const_value)(tokeniser) || tokeniser.error("Const lacks a value");
+ tokens.termination =
+ tokeniser.consume(";") ||
+ tokeniser.error("Unterminated const, expected `;`");
+ const ret = new Constant({ source: tokeniser.source, tokens });
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.autoParenter)(ret).idlType = idlType;
+ return ret;
+ }
+
+ get type() {
+ return "const";
+ }
+ get name() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.unescape)(this.tokens.name.value);
+ }
+ get value() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.const_data)(this.tokens.value);
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ const { parent } = this;
+ return w.ts.definition(
+ w.ts.wrap([
+ this.extAttrs.write(w),
+ w.token(this.tokens.base),
+ w.ts.type(this.idlType.write(w)),
+ w.name_token(this.tokens.name, { data: this, parent }),
+ w.token(this.tokens.assign),
+ w.token(this.tokens.value),
+ w.token(this.tokens.termination),
+ ]),
+ { data: this, parent }
+ );
+ }
+}
+
+
+/***/ }),
+/* 22 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "IterableLike": () => (/* binding */ IterableLike)
+/* harmony export */ });
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
+
+
+
+class IterableLike extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser) {
+ const start_position = tokeniser.position;
+ const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)(
+ new IterableLike({ source: tokeniser.source, tokens: {} })
+ );
+ const { tokens } = ret;
+ tokens.readonly = tokeniser.consume("readonly");
+ if (!tokens.readonly) {
+ tokens.async = tokeniser.consume("async");
+ }
+ tokens.base = tokens.readonly
+ ? tokeniser.consume("maplike", "setlike")
+ : tokens.async
+ ? tokeniser.consume("iterable")
+ : tokeniser.consume("iterable", "maplike", "setlike");
+ if (!tokens.base) {
+ tokeniser.unconsume(start_position);
+ return;
+ }
+
+ const { type } = ret;
+ const secondTypeRequired = type === "maplike";
+ const secondTypeAllowed = secondTypeRequired || type === "iterable";
+ const argumentAllowed = ret.async && type === "iterable";
+
+ tokens.open =
+ tokeniser.consume("<") ||
+ tokeniser.error(`Missing less-than sign \`<\` in ${type} declaration`);
+ const first =
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser) ||
+ tokeniser.error(`Missing a type argument in ${type} declaration`);
+ ret.idlType = [first];
+ ret.arguments = [];
+
+ if (secondTypeAllowed) {
+ first.tokens.separator = tokeniser.consume(",");
+ if (first.tokens.separator) {
+ ret.idlType.push((0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser));
+ } else if (secondTypeRequired) {
+ tokeniser.error(`Missing second type argument in ${type} declaration`);
+ }
+ }
+
+ tokens.close =
+ tokeniser.consume(">") ||
+ tokeniser.error(`Missing greater-than sign \`>\` in ${type} declaration`);
+
+ if (tokeniser.probe("(")) {
+ if (argumentAllowed) {
+ tokens.argsOpen = tokeniser.consume("(");
+ ret.arguments.push(...(0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.argument_list)(tokeniser));
+ tokens.argsClose =
+ tokeniser.consume(")") ||
+ tokeniser.error("Unterminated async iterable argument list");
+ } else {
+ tokeniser.error(`Arguments are only allowed for \`async iterable\``);
+ }
+ }
+
+ tokens.termination =
+ tokeniser.consume(";") ||
+ tokeniser.error(`Missing semicolon after ${type} declaration`);
+
+ return ret.this;
+ }
+
+ get type() {
+ return this.tokens.base.value;
+ }
+ get readonly() {
+ return !!this.tokens.readonly;
+ }
+ get async() {
+ return !!this.tokens.async;
+ }
+
+ *validate(defs) {
+ for (const type of this.idlType) {
+ yield* type.validate(defs);
+ }
+ for (const argument of this.arguments) {
+ yield* argument.validate(defs);
+ }
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ return w.ts.definition(
+ w.ts.wrap([
+ this.extAttrs.write(w),
+ w.token(this.tokens.readonly),
+ w.token(this.tokens.async),
+ w.token(this.tokens.base, w.ts.generic),
+ w.token(this.tokens.open),
+ w.ts.wrap(this.idlType.map((t) => t.write(w))),
+ w.token(this.tokens.close),
+ w.token(this.tokens.argsOpen),
+ w.ts.wrap(this.arguments.map((arg) => arg.write(w))),
+ w.token(this.tokens.argsClose),
+ w.token(this.tokens.termination),
+ ]),
+ { data: this, parent: this.parent }
+ );
+ }
+}
+
+
+/***/ }),
+/* 23 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "checkInterfaceMemberDuplication": () => (/* binding */ checkInterfaceMemberDuplication)
+/* harmony export */ });
+/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3);
+
+
+/**
+ * @param {import("../validator.js").Definitions} defs
+ * @param {import("../productions/container.js").Container} i
+ */
+function* checkInterfaceMemberDuplication(defs, i) {
+ const opNames = groupOperationNames(i);
+ const partials = defs.partials.get(i.name) || [];
+ const mixins = defs.mixinMap.get(i.name) || [];
+ for (const ext of [...partials, ...mixins]) {
+ const additions = getOperations(ext);
+ const statics = additions.filter((a) => a.special === "static");
+ const nonstatics = additions.filter((a) => a.special !== "static");
+ yield* checkAdditions(statics, opNames.statics, ext, i);
+ yield* checkAdditions(nonstatics, opNames.nonstatics, ext, i);
+ statics.forEach((op) => opNames.statics.add(op.name));
+ nonstatics.forEach((op) => opNames.nonstatics.add(op.name));
+ }
+
+ /**
+ * @param {import("../productions/operation.js").Operation[]} additions
+ * @param {Set<string>} existings
+ * @param {import("../productions/container.js").Container} ext
+ * @param {import("../productions/container.js").Container} base
+ */
+ function* checkAdditions(additions, existings, ext, base) {
+ for (const addition of additions) {
+ const { name } = addition;
+ if (name && existings.has(name)) {
+ const isStatic = addition.special === "static" ? "static " : "";
+ const message = `The ${isStatic}operation "${name}" has already been defined for the base interface "${base.name}" either in itself or in a mixin`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.validationError)(
+ addition.tokens.name,
+ ext,
+ "no-cross-overload",
+ message
+ );
+ }
+ }
+ }
+
+ /**
+ * @param {import("../productions/container.js").Container} i
+ * @returns {import("../productions/operation.js").Operation[]}
+ */
+ function getOperations(i) {
+ return i.members.filter(({ type }) => type === "operation");
+ }
+
+ /**
+ * @param {import("../productions/container.js").Container} i
+ */
+ function groupOperationNames(i) {
+ const ops = getOperations(i);
+ return {
+ statics: new Set(
+ ops.filter((op) => op.special === "static").map((op) => op.name)
+ ),
+ nonstatics: new Set(
+ ops.filter((op) => op.special !== "static").map((op) => op.name)
+ ),
+ };
+ }
+}
+
+
+/***/ }),
+/* 24 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Constructor": () => (/* binding */ Constructor)
+/* harmony export */ });
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
+
+
+
+class Constructor extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser) {
+ const base = tokeniser.consume("constructor");
+ if (!base) {
+ return;
+ }
+ /** @type {Base["tokens"]} */
+ const tokens = { base };
+ tokens.open =
+ tokeniser.consume("(") ||
+ tokeniser.error("No argument list in constructor");
+ const args = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.argument_list)(tokeniser);
+ tokens.close =
+ tokeniser.consume(")") || tokeniser.error("Unterminated constructor");
+ tokens.termination =
+ tokeniser.consume(";") ||
+ tokeniser.error("No semicolon after constructor");
+ const ret = new Constructor({ source: tokeniser.source, tokens });
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)(ret).arguments = args;
+ return ret;
+ }
+
+ get type() {
+ return "constructor";
+ }
+
+ *validate(defs) {
+ for (const argument of this.arguments) {
+ yield* argument.validate(defs);
+ }
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ const { parent } = this;
+ return w.ts.definition(
+ w.ts.wrap([
+ this.extAttrs.write(w),
+ w.token(this.tokens.base, w.ts.nameless, { data: this, parent }),
+ w.token(this.tokens.open),
+ w.ts.wrap(this.arguments.map((arg) => arg.write(w))),
+ w.token(this.tokens.close),
+ w.token(this.tokens.termination),
+ ]),
+ { data: this, parent }
+ );
+ }
+}
+
+
+/***/ }),
+/* 25 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Mixin": () => (/* binding */ Mixin)
+/* harmony export */ });
+/* harmony import */ var _container_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(20);
+/* harmony import */ var _constant_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(21);
+/* harmony import */ var _attribute_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(14);
+/* harmony import */ var _operation_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(13);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(4);
+
+
+
+
+
+
+class Mixin extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container {
+ /**
+ * @typedef {import("../tokeniser.js").Token} Token
+ *
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {Token} base
+ * @param {object} [options]
+ * @param {Token} [options.partial]
+ */
+ static parse(tokeniser, base, { partial } = {}) {
+ const tokens = { partial, base };
+ tokens.mixin = tokeniser.consume("mixin");
+ if (!tokens.mixin) {
+ return;
+ }
+ return _container_js__WEBPACK_IMPORTED_MODULE_0__.Container.parse(
+ tokeniser,
+ new Mixin({ source: tokeniser.source, tokens }),
+ {
+ allowedMembers: [
+ [_constant_js__WEBPACK_IMPORTED_MODULE_1__.Constant.parse],
+ [_helpers_js__WEBPACK_IMPORTED_MODULE_4__.stringifier],
+ [_attribute_js__WEBPACK_IMPORTED_MODULE_2__.Attribute.parse, { noInherit: true }],
+ [_operation_js__WEBPACK_IMPORTED_MODULE_3__.Operation.parse, { regular: true }],
+ ],
+ }
+ );
+ }
+
+ get type() {
+ return "interface mixin";
+ }
+}
+
+
+/***/ }),
+/* 26 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Dictionary": () => (/* binding */ Dictionary)
+/* harmony export */ });
+/* harmony import */ var _container_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(20);
+/* harmony import */ var _field_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(27);
+
+
+
+class Dictionary extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {object} [options]
+ * @param {import("../tokeniser.js").Token} [options.partial]
+ */
+ static parse(tokeniser, { partial } = {}) {
+ const tokens = { partial };
+ tokens.base = tokeniser.consume("dictionary");
+ if (!tokens.base) {
+ return;
+ }
+ return _container_js__WEBPACK_IMPORTED_MODULE_0__.Container.parse(
+ tokeniser,
+ new Dictionary({ source: tokeniser.source, tokens }),
+ {
+ inheritable: !partial,
+ allowedMembers: [[_field_js__WEBPACK_IMPORTED_MODULE_1__.Field.parse]],
+ }
+ );
+ }
+
+ get type() {
+ return "dictionary";
+ }
+}
+
+
+/***/ }),
+/* 27 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Field": () => (/* binding */ Field)
+/* harmony export */ });
+/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
+/* harmony import */ var _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(8);
+/* harmony import */ var _default_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(12);
+
+
+
+
+
+class Field extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser) {
+ /** @type {Base["tokens"]} */
+ const tokens = {};
+ const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)(new Field({ source: tokeniser.source, tokens }));
+ ret.extAttrs = _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__.ExtendedAttributes.parse(tokeniser);
+ tokens.required = tokeniser.consume("required");
+ ret.idlType =
+ (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser, "dictionary-type") ||
+ tokeniser.error("Dictionary member lacks a type");
+ tokens.name =
+ tokeniser.consumeKind("identifier") ||
+ tokeniser.error("Dictionary member lacks a name");
+ ret.default = _default_js__WEBPACK_IMPORTED_MODULE_3__.Default.parse(tokeniser);
+ if (tokens.required && ret.default)
+ tokeniser.error("Required member must not have a default");
+ tokens.termination =
+ tokeniser.consume(";") ||
+ tokeniser.error("Unterminated dictionary member, expected `;`");
+ return ret.this;
+ }
+
+ get type() {
+ return "field";
+ }
+ get name() {
+ return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(this.tokens.name.value);
+ }
+ get required() {
+ return !!this.tokens.required;
+ }
+
+ *validate(defs) {
+ yield* this.idlType.validate(defs);
+ }
+
+ /** @param {import("../writer.js").Writer} w */
+ write(w) {
+ const { parent } = this;
+ return w.ts.definition(
+ w.ts.wrap([
+ this.extAttrs.write(w),
+ w.token(this.tokens.required),
+ w.ts.type(this.idlType.write(w)),
+ w.name_token(this.tokens.name, { data: this, parent }),
+ this.default ? this.default.write(w) : "",
+ w.token(this.tokens.termination),
+ ]),
+ { data: this, parent }
+ );
+ }
+}
+
+
+/***/ }),
+/* 28 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Namespace": () => (/* binding */ Namespace)
+/* harmony export */ });
+/* harmony import */ var _container_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(20);
+/* harmony import */ var _attribute_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(14);
+/* harmony import */ var _operation_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(13);
+/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(3);
+/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(4);
+/* harmony import */ var _constant_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(21);
+
+
+
+
+
+
+
+class Namespace extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ * @param {object} [options]
+ * @param {import("../tokeniser.js").Token} [options.partial]
+ */
+ static parse(tokeniser, { partial } = {}) {
+ const tokens = { partial };
+ tokens.base = tokeniser.consume("namespace");
+ if (!tokens.base) {
+ return;
+ }
+ return _container_js__WEBPACK_IMPORTED_MODULE_0__.Container.parse(
+ tokeniser,
+ new Namespace({ source: tokeniser.source, tokens }),
+ {
+ allowedMembers: [
+ [_attribute_js__WEBPACK_IMPORTED_MODULE_1__.Attribute.parse, { noInherit: true, readonly: true }],
+ [_constant_js__WEBPACK_IMPORTED_MODULE_5__.Constant.parse],
+ [_operation_js__WEBPACK_IMPORTED_MODULE_2__.Operation.parse, { regular: true }],
+ ],
+ }
+ );
+ }
+
+ get type() {
+ return "namespace";
+ }
+
+ *validate(defs) {
+ if (
+ !this.partial &&
+ this.extAttrs.every((extAttr) => extAttr.name !== "Exposed")
+ ) {
+ const message = `Namespaces must have [Exposed] extended attribute. \
+To fix, add, for example, [Exposed=Window]. Please also consider carefully \
+if your namespace should also be exposed in a Worker scope. Refer to the \
+[WebIDL spec section on Exposed](https://heycam.github.io/webidl/#Exposed) \
+for more information.`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_3__.validationError)(
+ this.tokens.name,
+ this,
+ "require-exposed",
+ message,
+ {
+ autofix: (0,_helpers_js__WEBPACK_IMPORTED_MODULE_4__.autofixAddExposedWindow)(this),
+ }
+ );
+ }
+ yield* super.validate(defs);
+ }
+}
+
+
+/***/ }),
+/* 29 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "CallbackInterface": () => (/* binding */ CallbackInterface)
+/* harmony export */ });
+/* harmony import */ var _container_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(20);
+/* harmony import */ var _operation_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(13);
+/* harmony import */ var _constant_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(21);
+
+
+
+
+class CallbackInterface extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container {
+ /**
+ * @param {import("../tokeniser.js").Tokeniser} tokeniser
+ */
+ static parse(tokeniser, callback, { partial = null } = {}) {
+ const tokens = { callback };
+ tokens.base = tokeniser.consume("interface");
+ if (!tokens.base) {
+ return;
+ }
+ return _container_js__WEBPACK_IMPORTED_MODULE_0__.Container.parse(
+ tokeniser,
+ new CallbackInterface({ source: tokeniser.source, tokens }),
+ {
+ inheritable: !partial,
+ allowedMembers: [
+ [_constant_js__WEBPACK_IMPORTED_MODULE_2__.Constant.parse],
+ [_operation_js__WEBPACK_IMPORTED_MODULE_1__.Operation.parse, { regular: true }],
+ ],
+ }
+ );
+ }
+
+ get type() {
+ return "callback interface";
+ }
+}
+
+
+/***/ }),
+/* 30 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "Writer": () => (/* binding */ Writer),
+/* harmony export */ "write": () => (/* binding */ write)
+/* harmony export */ });
+function noop(arg) {
+ return arg;
+}
+
+const templates = {
+ wrap: (items) => items.join(""),
+ trivia: noop,
+ name: noop,
+ reference: noop,
+ type: noop,
+ generic: noop,
+ nameless: noop,
+ inheritance: noop,
+ definition: noop,
+ extendedAttribute: noop,
+ extendedAttributeReference: noop,
+};
+
+class Writer {
+ constructor(ts) {
+ this.ts = Object.assign({}, templates, ts);
+ }
+
+ /**
+ * @param {string} raw
+ * @param {object} options
+ * @param {string} [options.unescaped]
+ * @param {import("./productions/base.js").Base} [options.context]
+ * @returns
+ */
+ reference(raw, { unescaped, context }) {
+ if (!unescaped) {
+ unescaped = raw.startsWith("_") ? raw.slice(1) : raw;
+ }
+ return this.ts.reference(raw, unescaped, context);
+ }
+
+ /**
+ * @param {import("./tokeniser.js").Token} t
+ * @param {Function} wrapper
+ * @param {...any} args
+ * @returns
+ */
+ token(t, wrapper = noop, ...args) {
+ if (!t) {
+ return "";
+ }
+ const value = wrapper(t.value, ...args);
+ return this.ts.wrap([this.ts.trivia(t.trivia), value]);
+ }
+
+ reference_token(t, context) {
+ return this.token(t, this.reference.bind(this), { context });
+ }
+
+ name_token(t, arg) {
+ return this.token(t, this.ts.name, arg);
+ }
+
+ identifier(id, context) {
+ return this.ts.wrap([
+ this.reference_token(id.tokens.value, context),
+ this.token(id.tokens.separator),
+ ]);
+ }
+}
+
+function write(ast, { templates: ts = templates } = {}) {
+ ts = Object.assign({}, templates, ts);
+
+ const w = new Writer(ts);
+
+ return ts.wrap(ast.map((it) => it.write(w)));
+}
+
+
+/***/ }),
+/* 31 */
+/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
+
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "validate": () => (/* binding */ validate)
+/* harmony export */ });
+/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3);
+
+
+function getMixinMap(all, unique) {
+ const map = new Map();
+ const includes = all.filter((def) => def.type === "includes");
+ for (const include of includes) {
+ const mixin = unique.get(include.includes);
+ if (!mixin) {
+ continue;
+ }
+ const array = map.get(include.target);
+ if (array) {
+ array.push(mixin);
+ } else {
+ map.set(include.target, [mixin]);
+ }
+ }
+ return map;
+}
+
+/**
+ * @typedef {ReturnType<typeof groupDefinitions>} Definitions
+ */
+function groupDefinitions(all) {
+ const unique = new Map();
+ const duplicates = new Set();
+ const partials = new Map();
+ for (const def of all) {
+ if (def.partial) {
+ const array = partials.get(def.name);
+ if (array) {
+ array.push(def);
+ } else {
+ partials.set(def.name, [def]);
+ }
+ continue;
+ }
+ if (!def.name) {
+ continue;
+ }
+ if (!unique.has(def.name)) {
+ unique.set(def.name, def);
+ } else {
+ duplicates.add(def);
+ }
+ }
+ return {
+ all,
+ unique,
+ partials,
+ duplicates,
+ mixinMap: getMixinMap(all, unique),
+ cache: {
+ typedefIncludesDictionary: new WeakMap(),
+ dictionaryIncludesRequiredField: new WeakMap(),
+ },
+ };
+}
+
+function* checkDuplicatedNames({ unique, duplicates }) {
+ for (const dup of duplicates) {
+ const { name } = dup;
+ const message = `The name "${name}" of type "${
+ unique.get(name).type
+ }" was already seen`;
+ yield (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.validationError)(dup.tokens.name, dup, "no-duplicate", message);
+ }
+}
+
+function* validateIterable(ast) {
+ const defs = groupDefinitions(ast);
+ for (const def of defs.all) {
+ if (def.validate) {
+ yield* def.validate(defs);
+ }
+ }
+ yield* checkDuplicatedNames(defs);
+}
+
+// Remove this once all of our support targets expose `.flat()` by default
+function flatten(array) {
+ if (array.flat) {
+ return array.flat();
+ }
+ return [].concat(...array);
+}
+
+/**
+ * @param {import("./productions/base.js").Base[]} ast
+ * @return {import("./error.js").WebIDLErrorData[]} validation errors
+ */
+function validate(ast) {
+ return [...validateIterable(flatten(ast))];
+}
+
+
+/***/ })
+/******/ ]);
+/************************************************************************/
+/******/ // The module cache
+/******/ var __webpack_module_cache__ = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/ // Check if module is in cache
+/******/ var cachedModule = __webpack_module_cache__[moduleId];
+/******/ if (cachedModule !== undefined) {
+/******/ return cachedModule.exports;
+/******/ }
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = __webpack_module_cache__[moduleId] = {
+/******/ // no module.id needed
+/******/ // no module.loaded needed
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/************************************************************************/
+/******/ /* webpack/runtime/define property getters */
+/******/ (() => {
+/******/ // define getter functions for harmony exports
+/******/ __webpack_require__.d = (exports, definition) => {
+/******/ for(var key in definition) {
+/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
+/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
+/******/ }
+/******/ }
+/******/ };
+/******/ })();
+/******/
+/******/ /* webpack/runtime/hasOwnProperty shorthand */
+/******/ (() => {
+/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
+/******/ })();
+/******/
+/******/ /* webpack/runtime/make namespace object */
+/******/ (() => {
+/******/ // define __esModule on exports
+/******/ __webpack_require__.r = (exports) => {
+/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
+/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
+/******/ }
+/******/ Object.defineProperty(exports, '__esModule', { value: true });
+/******/ };
+/******/ })();
+/******/
+/************************************************************************/
+var __webpack_exports__ = {};
+// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
+(() => {
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "WebIDLParseError": () => (/* reexport safe */ _lib_tokeniser_js__WEBPACK_IMPORTED_MODULE_3__.WebIDLParseError),
+/* harmony export */ "parse": () => (/* reexport safe */ _lib_webidl2_js__WEBPACK_IMPORTED_MODULE_0__.parse),
+/* harmony export */ "validate": () => (/* reexport safe */ _lib_validator_js__WEBPACK_IMPORTED_MODULE_2__.validate),
+/* harmony export */ "write": () => (/* reexport safe */ _lib_writer_js__WEBPACK_IMPORTED_MODULE_1__.write)
+/* harmony export */ });
+/* harmony import */ var _lib_webidl2_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
+/* harmony import */ var _lib_writer_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(30);
+/* harmony import */ var _lib_validator_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(31);
+/* harmony import */ var _lib_tokeniser_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(2);
+
+
+
+
+
+})();
+
+/******/ return __webpack_exports__;
+/******/ })()
+;
+});
+//# sourceMappingURL=webidl2.js.map \ No newline at end of file
diff --git a/testing/web-platform/tests/resources/webidl2/lib/webidl2.js.headers b/testing/web-platform/tests/resources/webidl2/lib/webidl2.js.headers
new file mode 100644
index 0000000000..6805c323df
--- /dev/null
+++ b/testing/web-platform/tests/resources/webidl2/lib/webidl2.js.headers
@@ -0,0 +1 @@
+Content-Type: text/javascript; charset=utf-8