Open the console inside the developer tools. It should contain entries whose contents are:
+
default: 1
+
default: 1
+
default: 1
+
default: 1
+
default: 1
+
default: 1
+
default: 1
+
default: 1
+
a label: 1
+
a label: 1
+
[some warning message indicating that a count for label "b" does not exist]
+
+
+
+
diff --git a/test/parallel/test-whatwg-console-is-a-namespace.js b/test/fixtures/wpt/console/console-is-a-namespace.any.js
similarity index 70%
rename from test/parallel/test-whatwg-console-is-a-namespace.js
rename to test/fixtures/wpt/console/console-is-a-namespace.any.js
index 8d6569ad6d6521..a4aae7ffce8ac8 100644
--- a/test/parallel/test-whatwg-console-is-a-namespace.js
+++ b/test/fixtures/wpt/console/console-is-a-namespace.any.js
@@ -1,20 +1,4 @@
-'use strict';
-
-require('../common');
-
-const { test, assert_equals, assert_true, assert_false } =
- require('../common/wpt');
-
-const self = global;
-
-/* eslint-disable quotes, max-len */
-
-/* The following tests should not be modified as they are copied */
-/* WPT Refs:
- https://github.com/w3c/web-platform-tests/blob/40e451c/console/console-is-a-namespace.any.js
- License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html
-*/
-
+"use strict";
// https://heycam.github.io/webidl/#es-namespaces
// https://console.spec.whatwg.org/#console-namespace
@@ -41,5 +25,3 @@ test(() => {
assert_equals(Object.getOwnPropertyNames(prototype1).length, 0, "The [[Prototype]] must have no properties");
assert_equals(prototype2, Object.prototype, "The [[Prototype]]'s [[Prototype]] must be %ObjectPrototype%");
}, "The prototype chain must be correct");
-
-/* eslint-enable */
diff --git a/test/parallel/test-whatwg-console-label-conversion.js b/test/fixtures/wpt/console/console-label-conversion.any.js
similarity index 64%
rename from test/parallel/test-whatwg-console-label-conversion.js
rename to test/fixtures/wpt/console/console-label-conversion.any.js
index 9131a0c175d11c..1fb269d4061c61 100644
--- a/test/parallel/test-whatwg-console-label-conversion.js
+++ b/test/fixtures/wpt/console/console-label-conversion.any.js
@@ -1,18 +1,4 @@
-'use strict';
-
-require('../common');
-
-const { test, assert_true, assert_throws } =
- require('../common/wpt');
-
-/* eslint-disable max-len, object-curly-spacing */
-
-/* The following tests should not be modified as they are copied */
-/* WPT Refs:
- https://github.com/web-platform-tests/wpt/blob/6f0a96ed650935b17b6e5d277889cfbe0ccc103e/console/console-label-conversion.any.js
- License: https://github.com/web-platform-tests/wpt/blob/6f0a96ed650935b17b6e5d277889cfbe0ccc103e/LICENSE.md
-*/
-
+"use strict";
// https://console.spec.whatwg/org/#counting
// https://console.spec.whatwg/org/#timing
@@ -41,5 +27,3 @@ for (const method of methods) {
}, `${method} must re-throw any exceptions thrown by label.toString() conversion`);
}, `console.${method}() throws exceptions generated by erroneous label.toString() conversion`);
}
-
-/* eslint-enable */
diff --git a/test/fixtures/wpt/console/console-number-format-specifiers-symbol-manual.html b/test/fixtures/wpt/console/console-number-format-specifiers-symbol-manual.html
new file mode 100644
index 00000000000000..f77b84e51595f3
--- /dev/null
+++ b/test/fixtures/wpt/console/console-number-format-specifiers-symbol-manual.html
@@ -0,0 +1,26 @@
+
+
+
+Console Number Format Specifiers on Symbols
+
+
+
+
+
+
Open the console inside the developer tools. It should contain 15 entries, each of which are:
+
NaN
+
+
+
+
diff --git a/test/fixtures/wpt/console/console-string-format-specifier-symbol-manual.html b/test/fixtures/wpt/console/console-string-format-specifier-symbol-manual.html
new file mode 100644
index 00000000000000..3a1b93f7352174
--- /dev/null
+++ b/test/fixtures/wpt/console/console-string-format-specifier-symbol-manual.html
@@ -0,0 +1,23 @@
+
+
+
+Console String Format Specifier on Symbols
+
+
+
+
+
+
Open the console inside the developer tools. It should contain five entries, each of which are:
+
Symbol(description)
+
+
+
+
diff --git a/test/parallel/test-whatwg-console-tests-historical.js b/test/fixtures/wpt/console/console-tests-historical.any.js
similarity index 59%
rename from test/parallel/test-whatwg-console-tests-historical.js
rename to test/fixtures/wpt/console/console-tests-historical.any.js
index d4a998b07061b9..4c4d4c276d0cd4 100644
--- a/test/parallel/test-whatwg-console-tests-historical.js
+++ b/test/fixtures/wpt/console/console-tests-historical.any.js
@@ -1,18 +1,3 @@
-'use strict';
-
-require('../common');
-
-const { test, assert_equals } =
- require('../common/wpt');
-
-/* eslint-disable max-len, quotes */
-
-/* The following tests should not be modified as they are copied */
-/* WPT Refs:
- https://github.com/web-platform-tests/wpt/blob/6f0a96ed650935b17b6e5d277889cfbe0ccc103e/console/console-tests-historical.any.js
- License: https://github.com/web-platform-tests/wpt/blob/6f0a96ed650935b17b6e5d277889cfbe0ccc103e/LICENSE.md
-*/
-
/**
* These tests assert the non-existence of certain
* legacy Console methods that are not included in
@@ -32,5 +17,3 @@ test(() => {
test(() => {
assert_equals(console.markTimeline, undefined, "console.markTimeline should be undefined");
}, "'markTimeline' function should not exist on the console object");
-
-/* eslint-enable */
diff --git a/test/fixtures/wpt/console/console-timing-logging-manual.html b/test/fixtures/wpt/console/console-timing-logging-manual.html
new file mode 100644
index 00000000000000..3b9e5cea21da8e
--- /dev/null
+++ b/test/fixtures/wpt/console/console-timing-logging-manual.html
@@ -0,0 +1,70 @@
+
+
+
+Console Timing Methods - Logging Manual Test
+
+
+
+
+
+
Open the console inside the developer tools. It should contain entries whose contents are:
+
default: <some time>
+
default: <some time>
+
+
default: <some time>
+
default: <some time> extra data
+
default: <some time>
+
+
default: <some time>
+
default: <some time> extra data
+
default: <some time>
+
+
default: <some time>
+
default: <some time> extra data
+
default: <some time>
+
+
custom toString(): <some time>
+
custom toString(): <some time> extra data
+
custom toString(): <some time>
+
+
a label: <some time>
+
a label: <some time> extra data
+
a label: <some time>
+
+
[some warning message indicating that a timer for label "b" does not exist]
+
+
+
+
diff --git a/test/fixtures/wpt/console/idlharness.any.js b/test/fixtures/wpt/console/idlharness.any.js
new file mode 100644
index 00000000000000..1e7ba76ecddca2
--- /dev/null
+++ b/test/fixtures/wpt/console/idlharness.any.js
@@ -0,0 +1,9 @@
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+
+// https://console.spec.whatwg.org/
+
+idl_test(
+ ['console'],
+ [] // no deps
+);
diff --git a/test/fixtures/wpt/interfaces/console.idl b/test/fixtures/wpt/interfaces/console.idl
new file mode 100644
index 00000000000000..53130711a1173f
--- /dev/null
+++ b/test/fixtures/wpt/interfaces/console.idl
@@ -0,0 +1,34 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into reffy-reports
+// (https://github.com/tidoust/reffy-reports)
+// Source: Console Standard (https://console.spec.whatwg.org/)
+
+[Exposed=(Window,Worker,Worklet)]
+namespace console { // but see namespace object requirements below
+ // Logging
+ void assert(optional boolean condition = false, any... data);
+ void clear();
+ void debug(any... data);
+ void error(any... data);
+ void info(any... data);
+ void log(any... data);
+ void table(any tabularData, optional sequence properties);
+ void trace(any... data);
+ void warn(any... data);
+ void dir(any item, optional object? options);
+ void dirxml(any... data);
+
+ // Counting
+ void count(optional DOMString label = "default");
+ void countReset(optional DOMString label = "default");
+
+ // Grouping
+ void group(any... data);
+ void groupCollapsed(any... data);
+ void groupEnd();
+
+ // Timing
+ void time(optional DOMString label = "default");
+ void timeLog(optional DOMString label = "default", any... data);
+ void timeEnd(optional DOMString label = "default");
+};
diff --git a/test/fixtures/wpt/interfaces/url.idl b/test/fixtures/wpt/interfaces/url.idl
new file mode 100644
index 00000000000000..998052da6ef1b3
--- /dev/null
+++ b/test/fixtures/wpt/interfaces/url.idl
@@ -0,0 +1,40 @@
+// GENERATED CONTENT - DO NOT EDIT
+// Content was automatically extracted by Reffy into reffy-reports
+// (https://github.com/tidoust/reffy-reports)
+// Source: URL Standard (https://url.spec.whatwg.org/)
+
+[Constructor(USVString url, optional USVString base),
+ Exposed=(Window,Worker),
+ LegacyWindowAlias=webkitURL]
+interface URL {
+ stringifier attribute USVString href;
+ readonly attribute USVString origin;
+ attribute USVString protocol;
+ attribute USVString username;
+ attribute USVString password;
+ attribute USVString host;
+ attribute USVString hostname;
+ attribute USVString port;
+ attribute USVString pathname;
+ attribute USVString search;
+ [SameObject] readonly attribute URLSearchParams searchParams;
+ attribute USVString hash;
+
+ USVString toJSON();
+};
+
+[Constructor(optional (sequence> or record or USVString) init = ""),
+ Exposed=(Window,Worker)]
+interface URLSearchParams {
+ void append(USVString name, USVString value);
+ void delete(USVString name);
+ USVString? get(USVString name);
+ sequence getAll(USVString name);
+ boolean has(USVString name);
+ void set(USVString name, USVString value);
+
+ void sort();
+
+ iterable;
+ stringifier;
+};
diff --git a/test/fixtures/wpt/resources/.gitignore b/test/fixtures/wpt/resources/.gitignore
new file mode 100644
index 00000000000000..04fdeda1cc4ea1
--- /dev/null
+++ b/test/fixtures/wpt/resources/.gitignore
@@ -0,0 +1,3 @@
+ROBIN-TODO.txt
+scratch
+node_modules
diff --git a/test/fixtures/wpt/resources/.htaccess b/test/fixtures/wpt/resources/.htaccess
new file mode 100644
index 00000000000000..fd46101ca0099e
--- /dev/null
+++ b/test/fixtures/wpt/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/test/fixtures/wpt/resources/LICENSE b/test/fixtures/wpt/resources/LICENSE
new file mode 100644
index 00000000000000..45896e6be2bd51
--- /dev/null
+++ b/test/fixtures/wpt/resources/LICENSE
@@ -0,0 +1,30 @@
+W3C 3-clause BSD License
+
+http://www.w3.org/Consortium/Legal/2008/03-bsd-license.html
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of works must retain the original copyright notice,
+ this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the original copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of the W3C nor the names of its contributors may be
+ used to endorse or promote products derived from this work without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/test/fixtures/wpt/resources/META.yml b/test/fixtures/wpt/resources/META.yml
new file mode 100644
index 00000000000000..8f988f99a82e09
--- /dev/null
+++ b/test/fixtures/wpt/resources/META.yml
@@ -0,0 +1,3 @@
+suggested_reviewers:
+ - jgraham
+ - gsnedders
diff --git a/test/fixtures/wpt/resources/check-layout-th.js b/test/fixtures/wpt/resources/check-layout-th.js
new file mode 100644
index 00000000000000..928b0cf2a1041e
--- /dev/null
+++ b/test/fixtures/wpt/resources/check-layout-th.js
@@ -0,0 +1,196 @@
+(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) || Math.abs(actual - expected) >= 1) {
+ assert_equals(actual, Number(expected), message);
+ }
+}
+
+function checkExpectedValues(t, node, prefix)
+{
+ 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 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;
+
+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 = "\n" + container.outerHTML + "\n";
+ var passed = false;
+ try {
+ checkedLayout |= checkExpectedValues(t, node.parentNode, prefix);
+ checkedLayout |= checkSubtreeExpectedValues(t, node, prefix);
+ passed = true;
+ } finally {
+ checkedLayout |= !passed;
+ }
+ }, selectorList + ' ' + String(++testNumber));
+ });
+ if (!checkedLayout) {
+ console.error("No valid data-* attributes found in selector list : " + selectorList);
+ }
+ if (callDone)
+ done();
+};
+
+})();
diff --git a/test/fixtures/wpt/resources/idlharness.js b/test/fixtures/wpt/resources/idlharness.js
new file mode 100644
index 00000000000000..40a5fa59cd209d
--- /dev/null
+++ b/test/fixtures/wpt/resources/idlharness.js
@@ -0,0 +1,3180 @@
+/*
+Distributed under both the W3C Test Suite License [1] and the W3C
+3-clause BSD License [2]. To contribute to a W3C Test Suite, see the
+policies and contribution forms [3].
+
+[1] http://www.w3.org/Consortium/Legal/2008/04-testsuite-license
+[2] http://www.w3.org/Consortium/Legal/2008/03-bsd-license
+[3] http://www.w3.org/2004/10/27-testcases
+*/
+
+/* 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); });
+}
+
+function throwOrReject(a_test, operation, fn, obj, args, message, cb)
+{
+ if (operation.idlType.generic !== "Promise") {
+ assert_throws(new TypeError(), function() {
+ fn.apply(obj, args);
+ }, message);
+ cb();
+ } else {
+ try {
+ promise_rejects(a_test, new 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();
+ }
+ };
+}
+
+var fround =
+(function(){
+ if (Math.fround) return Math.fround;
+
+ var arr = new Float32Array(1);
+ return function fround(n) {
+ arr[0] = n;
+ return arr[0];
+ };
+})();
+
+/// 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 implements statement that depends
+ * on a later one. Save these up and handle them right before we run
+ * tests.
+ *
+ * .partials is simply an array of objects from WebIDLParser.js'
+ * "partialinterface" production. .implements maps strings to arrays of
+ * strings, such that
+ *
+ * A implements B;
+ * A implements C;
+ * D implements E;
+ *
+ * results in this["implements"] = { A: ["B", "C"], D: ["E"] }.
+ *
+ * Similarly,
+ *
+ * interface A : B {};
+ * interface B : C {};
+ *
+ * results in this["inheritance"] = { A: "B", B: "C" }
+ */
+ this.partials = [];
+ this["implements"] = {};
+ this["includes"] = {};
+ this["inheritance"] = {};
+};
+
+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)
+{
+ const parsed_idls = WebIDL2.parse(raw_idls);
+ const new_options = { only: [] }
+
+ const all_deps = new Set();
+ Object.values(this.inheritance).forEach(v => all_deps.add(v));
+ Object.entries(this.implements).forEach(([k, v]) => {
+ all_deps.add(k);
+ all_deps.add(v);
+ });
+ // NOTE: If 'A includes B' for B that we care about, then A is also a dep.
+ Object.keys(this.includes).forEach(k => {
+ all_deps.add(k);
+ this.includes[k].forEach(v => all_deps.add(v));
+ });
+ this.partials.map(p => p.name).forEach(v => all_deps.add(v));
+ // Add the attribute idlTypes of all the nested members of all tested idls.
+ for (const obj of [this.members, this.partials]) {
+ const tested = Object.values(obj).filter(m => !m.untested && m.members);
+ for (const parsed of tested) {
+ for (const attr of Object.values(parsed.members).filter(m => !m.untested && m.type === 'attribute')) {
+ all_deps.add(attr.idlType.idlType);
+ }
+ }
+ }
+
+ if (options && options.except && options.only) {
+ throw new IdlHarnessError("The only and except options can't be used together.");
+ }
+
+ const should_skip = 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);
+ }
+ // Record of skipped items, in case we later determine they are a dependency.
+ // Maps name -> [parsed_idl, ...]
+ const skipped = new Map();
+ const process = function(parsed) {
+ var deps = [];
+ if (parsed.name) {
+ deps.push(parsed.name);
+ } else if (parsed.type === "implements") {
+ deps.push(parsed.target);
+ deps.push(parsed.implements);
+ } else if (parsed.type === "includes") {
+ deps.push(parsed.target);
+ deps.push(parsed.includes);
+ }
+
+ deps = deps.filter(function(name) {
+ if (!name || should_skip(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)) {
+ skipped.has(name)
+ ? skipped.get(name).push(parsed)
+ : skipped.set(name, [parsed]);
+ }
+ return false;
+ }
+ return true;
+ }.bind(this));
+
+ deps.forEach(function(name) {
+ new_options.only.push(name);
+
+ const follow_up = new Set();
+ for (const dep_type of ["inheritance", "implements", "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 is being tested.
+ if (dep_type !== "includes"
+ || (inheriting in this.members && !this.members[inheriting].untested)) {
+ deps.push(inheritor);
+ }
+ for (const dep of deps) {
+ new_options.only.push(dep);
+ all_deps.add(dep);
+ follow_up.add(dep);
+ }
+ }
+ }
+
+ for (const deferred of follow_up) {
+ if (skipped.has(deferred)) {
+ const next = skipped.get(deferred);
+ 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 == "implements")
+ {
+ if (should_skip(parsed_idl.target))
+ {
+ return;
+ }
+ if (!(parsed_idl.target in this["implements"]))
+ {
+ this["implements"][parsed_idl.target] = [];
+ }
+ this["implements"][parsed_idl.target].push(parsed_idl["implements"]);
+ return;
+ }
+
+ if (parsed_idl.type == "includes")
+ {
+ if (should_skip(parsed_idl.target))
+ {
+ return;
+ }
+ if (!(parsed_idl.target in this["includes"]))
+ {
+ this["includes"][parsed_idl.target] = [];
+ }
+ this["includes"][parsed_idl.target].push(parsed_idl["includes"]);
+ 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);
+ }
+
+ if (parsed_idl["inheritance"]) {
+ // NOTE: Clash should be impossible (would require redefinition of parsed_idl.name).
+ if (parsed_idl.name in this["inheritance"]
+ && parsed_idl["inheritance"] != this["inheritance"][parsed_idl.name]) {
+ throw new IdlHarnessError(
+ `Inheritance for ${parsed_idl.name} was already defined`);
+ }
+ this["inheritance"][parsed_idl.name] = parsed_idl["inheritance"];
+ }
+
+ 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":
+ // TODO
+ console.log("callback not yet supported");
+ 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.recursively_get_implements = function(interface_name)
+{
+ /**
+ * Helper function for test(). Returns an array of things that implement
+ * interface_name, so if the IDL contains
+ *
+ * A implements B;
+ * B implements C;
+ * B implements D;
+ *
+ * then recursively_get_implements("A") should return ["B", "C", "D"].
+ */
+ var ret = this["implements"][interface_name];
+ if (ret === undefined)
+ {
+ return [];
+ }
+ for (var i = 0; i < this["implements"][interface_name].length; i++)
+ {
+ ret = ret.concat(this.recursively_get_implements(ret[i]));
+ if (ret.indexOf(ret[i]) != ret.lastIndexOf(ret[i]))
+ {
+ throw new IdlHarnessError("Circular implements statements involving " + ret[i]);
+ }
+ }
+ return ret;
+};
+
+IdlArray.prototype.recursively_get_includes = function(interface_name)
+{
+ /**
+ * Helper function for test(). Returns an array of things that implement
+ * interface_name, so if the IDL contains
+ *
+ * A includes B;
+ * B includes C;
+ * B includes D;
+ *
+ * then recursively_get_includes("A") should return ["B", "C", "D"].
+ */
+ var ret = this["includes"][interface_name];
+ if (ret === undefined)
+ {
+ return [];
+ }
+ for (var i = 0; i < this["includes"][interface_name].length; i++)
+ {
+ ret = ret.concat(this.recursively_get_includes(ret[i]));
+ if (ret.indexOf(ret[i]) != ret.lastIndexOf(ret[i]))
+ {
+ throw new IdlHarnessError("Circular includes statements involving " + ret[i]);
+ }
+ }
+ return ret;
+};
+
+IdlArray.prototype.is_json_type = function(type)
+{
+ /**
+ * Checks whether type is a JSON type as per
+ * https://heycam.github.io/webidl/#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);
+ }
+
+ 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 "Float32Array":
+ 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) {
+ var stack = thing.get_inheritance_stack();
+ var map = new Map();
+ while (stack.length)
+ {
+ stack.pop().members.forEach(function(m) {
+ 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 or consequential interfaces.
+ if (thing instanceof IdlInterface) {
+ var base;
+ while (thing)
+ {
+ if (thing.has_to_json_regular_operation()) { return true; }
+ var mixins = this.implements[thing.name] || 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) {
+ var set = exposed[0].rhs.value;
+ // Could be a list or a string.
+ if (typeof set == "string") {
+ set = [ set ];
+ }
+ result = new Set(set);
+ }
+ if (result && result.has("Worker")) {
+ result.delete("Worker");
+ result.add("DedicatedWorker");
+ result.add("ServiceWorker");
+ result.add("SharedWorker");
+ }
+ return result;
+}
+
+function exposed_in(globals) {
+ if ('document' 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");
+ }
+ 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 the partial interfaces and implements statements we
+ // encountered.
+ this.collapse_partials();
+
+ for (var lhs in this["implements"])
+ {
+ this.recursively_get_implements(lhs).forEach(function(rhs)
+ {
+ var errStr = lhs + " implements " + 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.";
+ this.members[rhs].members.forEach(function(member)
+ {
+ this.members[lhs].members.push(new IdlInterfaceMember(member));
+ }.bind(this));
+ }.bind(this));
+ }
+ this["implements"] = {};
+
+ for (var lhs in this["includes"])
+ {
+ this.recursively_get_includes(lhs).forEach(function(rhs)
+ {
+ 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.";
+ this.members[rhs].members.forEach(function(member)
+ {
+ this.members[lhs].members.push(new IdlInterfaceMember(member));
+ }.bind(this));
+ }.bind(this));
+ }
+ this["includes"] = {};
+
+ // 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_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.collapse_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);
+
+ let partialTestName = parsed_idl.name;
+ if (!parsed_idl.untested) {
+ // Ensure unique test name in case of multiple partials.
+ let partialTestCount = 1;
+ if (testedPartials.has(parsed_idl.name)) {
+ partialTestCount += testedPartials.get(parsed_idl.name);
+ partialTestName = `${partialTestName}[${partialTestCount}]`;
+ }
+ testedPartials.set(parsed_idl.name, partialTestCount);
+
+ test(function () {
+ assert_true(originalExists, `Original ${parsed_idl.type} should be defined`);
+
+ var expected = IdlInterface;
+ switch (parsed_idl.type) {
+ case 'interface': expected = IdlInterface; break;
+ case 'dictionary': expected = IdlDictionary; break;
+ case 'namespace': expected = IdlNamespace; 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]);
+ 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));
+ }
+ parsed_idl.members.forEach(function(member)
+ {
+ this.members[parsed_idl.name].members.push(new IdlInterfaceMember(member));
+ }.bind(this));
+ }.bind(this));
+ this.partials = [];
+}
+
+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.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.nullable && value === null)
+ {
+ // This is fine
+ return;
+ }
+
+ if (type.array)
+ {
+ // TODO: not supported yet
+ return;
+ }
+
+ if (type.generic === "sequence")
+ {
+ 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);
+ 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);
+ return;
+ }
+
+ type = type.idlType;
+
+ switch(type)
+ {
+ case "void":
+ 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, 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, 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 "object":
+ assert_in_array(typeof value, ["object", "function"], "wrong type: not object or function");
+ return;
+ }
+
+ if (!(type in this.members))
+ {
+ throw new IdlHarnessError("Unrecognized type " + type);
+ }
+
+ 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
+ // NoInterfaceObject 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("NoInterfaceObject")
+ && 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
+ {
+ throw new IdlHarnessError("Type " + type + " isn't an interface 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_inheritance_stack = function() {
+ return IdlInterface.prototype.get_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("Unforgeable")) {
+ this.members
+ .filter(function(m) { return !m["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://heycam.github.io/webidl/#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.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.has_extended_attribute("NoInterfaceObject")) {
+ throw new IdlHarnessError(this.name + " has no interface object due to NoInterfaceObject");
+ }
+
+ return this.get_interface_object_owner()[this.name];
+};
+
+IdlInterface.prototype.get_qualified_name = function() {
+ // https://heycam.github.io/webidl/#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");
+ });
+};
+
+IdlInterface.prototype.get_inheritance_stack = function() {
+ /**
+ * See https://heycam.github.io/webidl/#create-an-inheritance-stack
+ *
+ * Returns an array of IdlInterface objects which contains itself
+ * and all of its inherited interfaces.
+ *
+ * So given:
+ *
+ * A : B {};
+ * B : C {};
+ * C {};
+ *
+ * then A.get_inheritance_stack() should return [A, B, C],
+ * and B.get_inheritance_stack() should return [B, C].
+ *
+ * Note: as dictionary inheritance is expressed identically by the AST,
+ * this works just as well for getting a stack of inherited dictionaries.
+ */
+
+ var stack = [this];
+ var idl_interface = this;
+ while (idl_interface.base) {
+ var 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.push(base);
+ let dep_chain = stack.map(i => i.name).join(',');
+ throw new IdlHarnessError(`${this.name} has a circular dependency: ${dep_chain}`);
+ }
+ idl_interface = base;
+ stack.push(idl_interface);
+ }
+ return stack;
+};
+
+/**
+ * Implementation of
+ * https://heycam.github.io/webidl/#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(callback) {
+ var map = new Map(), isDefault = false;
+ this.traverse_inherited_and_consequential_interfaces(function(I) {
+ if (I.has_default_to_json_regular_operation()) {
+ isDefault = true;
+ I.members.forEach(function(m) {
+ if (!m.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;
+};
+
+/**
+ * Traverses inherited interfaces from the top down
+ * and imeplemented interfaces inside out.
+ * Invokes |callback| on each interface.
+ *
+ * This is an abstract implementation of the traversal
+ * algorithm specified in:
+ * https://heycam.github.io/webidl/#collect-attribute-values
+ * Given the following inheritance tree:
+ *
+ * F
+ * |
+ * C E - I
+ * | |
+ * B - D
+ * |
+ * G - A - H - J
+ *
+ * Invoking traverse_inherited_and_consequential_interfaces() on A
+ * would traverse the tree in the following order:
+ * C -> B -> F -> E -> I -> D -> A -> G -> H -> J
+ */
+
+IdlInterface.prototype.traverse_inherited_and_consequential_interfaces = function(callback) {
+ if (typeof callback != "function") {
+ throw new TypeError();
+ }
+ var stack = this.get_inheritance_stack();
+ _traverse_inherited_and_consequential_interfaces(stack, callback);
+};
+
+function _traverse_inherited_and_consequential_interfaces(stack, callback) {
+ var I = stack.pop();
+ callback(I);
+ var mixins = I.array["implements"][I.name] || I.array["includes"][I.name];
+ if (mixins) {
+ mixins.forEach(function(id) {
+ var mixin = I.array.members[id];
+ if (!mixin) {
+ throw new Error("Interface " + id + " not found (implemented by " + I.name + ")");
+ }
+ var interfaces = mixin.get_inheritance_stack();
+ _traverse_inherited_and_consequential_interfaces(interfaces, callback);
+ });
+ }
+ if (stack.length > 0) {
+ _traverse_inherited_and_consequential_interfaces(stack, callback);
+ }
+}
+
+IdlInterface.prototype.test = function()
+{
+ if (this.has_extended_attribute("NoInterfaceObject") || 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 (!this.exposed) {
+ 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.test_self = function()
+{
+ subsetTestByKey(this.name, test, function()
+ {
+ // This function tests WebIDL as of 2015-01-13.
+
+ // "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
+ // [NoInterfaceObject] extended attribute,
+ // a corresponding property MUST exist on the ECMAScript global object.
+ // 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 }."
+ if (this.is_callback() && !this.has_constants()) {
+ return;
+ }
+
+ // 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."
+ // Tested below if no constructor is defined. TODO: test constructors
+ // if defined.
+
+ // "* 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("NoInterfaceObject")) {
+ 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");
+ }
+
+ if (!this.has_extended_attribute("Constructor")) {
+ // "The internal [[Call]] method of the interface object behaves as
+ // follows . . .
+ //
+ // "If I was not declared with a [Constructor] extended attribute,
+ // then throw a TypeError."
+ var interface_object = this.get_interface_object();
+ assert_throws(new TypeError(), function() {
+ interface_object();
+ }, "interface object didn't throw TypeError when called as a function");
+ assert_throws(new 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.is_callback()) {
+ subsetTestByKey(this.name, test, function() {
+ // This function tests WebIDL as of 2014-10-25.
+ // https://heycam.github.io/webidl/#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.extAttrs
+ .filter(function(attr) { return attr.name == "Constructor"; });
+ 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.is_callback() || this.has_constants()) {
+ subsetTestByKey(this.name, test, function() {
+ // This function tests WebIDL as of 2015-11-17.
+ // https://heycam.github.io/webidl/#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.has("Window")) {
+ throw new IdlHarnessError("Invalid IDL: LegacyWindowAlias extended attribute on " + this.name + " which is not exposed in Window");
+ }
+ // TODO: when testing of [NoInterfaceObject] 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;
+ } 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");
+ }
+ // TODO: Test named constructors if I find any interfaces that have them.
+
+ subsetTestByKey(this.name, test, function()
+ {
+ // This function tests WebIDL as of 2015-01-21.
+ // https://heycam.github.io/webidl/#interface-object
+
+ if (this.is_callback() && !this.has_constants()) {
+ 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
+ // [NoInterfaceObject].)
+ // 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("NoInterfaceObject")) {
+ 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://heycam.github.io/webidl/#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.is_callback() && !this.has_constants()) {
+ 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 [NoInterfaceObject] 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.is_callback() && !this.has_constants()) {
+ 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(new 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() {
+ var setter = Object.getOwnPropertyDescriptor(
+ Object.prototype, '__proto__'
+ ).set;
+
+ try {
+ setter.call(obj, originalValue);
+ } catch (err) {}
+ });
+
+ assert_throws(new 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.is_callback() && !this.has_constants()) {
+ 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["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("LenientThis")) {
+ if (member.idlType.generic !== "Promise") {
+ assert_throws(new 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(a_test, new TypeError(),
+ this.get_interface_object().prototype[member.name])
+ .then(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.name +
+ "(" + member.arguments.map(
+ function(m) {return m.idlType.idlType; } ).join(", ")
+ +")");
+ a_test.step(function()
+ {
+ // This function tests WebIDL as of 2015-12-29.
+ // https://heycam.github.io/webidl/#es-operations
+
+ if (this.is_callback() && !this.has_constants()) {
+ 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["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");
+
+ // 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["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.add_iterable_members = function(member)
+{
+ this.members.push(new IdlInterfaceMember(
+ { type: "operation", name: "entries", idlType: "iterator", arguments: []}));
+ this.members.push(new IdlInterfaceMember(
+ { type: "operation", name: "keys", idlType: "iterator", arguments: []}));
+ this.members.push(new IdlInterfaceMember(
+ { type: "operation", name: "values", idlType: "iterator", arguments: []}));
+ this.members.push(new IdlInterfaceMember(
+ { type: "operation", name: "forEach", idlType: "void",
+ arguments:
+ [{ name: "callback", idlType: {idlType: "function"}},
+ { name: "thisValue", idlType: {idlType: "any"}, optional: true}]}));
+};
+
+IdlInterface.prototype.test_to_json_operation = function(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), "Test default toJSON operation of " + instanceName);
+ } 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), "Test toJSON operation of " + instanceName);
+ }
+};
+
+IdlInterface.prototype.test_member_iterable = function(member)
+{
+ var isPairIterator = member.idlType.length === 2;
+ subsetTestByKey(this.name, test, function()
+ {
+ var descriptor = Object.getOwnPropertyDescriptor(this.get_interface_object().prototype, Symbol.iterator);
+ assert_true(descriptor.writable, "property should be writable");
+ assert_true(descriptor.configurable, "property should be configurable");
+ assert_false(descriptor.enumerable, "property should not be enumerable");
+ assert_equals(this.get_interface_object().prototype[Symbol.iterator].name, isPairIterator ? "entries" : "values", "@@iterator function does not have the right name");
+ }.bind(this), "Testing Symbol.iterator property of iterable interface " + this.name);
+
+ if (isPairIterator) {
+ subsetTestByKey(this.name, test, function() {
+ assert_equals(this.get_interface_object().prototype[Symbol.iterator], this.get_interface_object().prototype["entries"], "entries method is not the same as @@iterator");
+ }.bind(this), "Testing pair iterable interface " + this.name);
+ } else {
+ subsetTestByKey(this.name, test, function() {
+ ["entries", "keys", "values", "forEach", Symbol.Iterator].forEach(function(property) {
+ assert_equals(this.get_interface_object().prototype[property], Array.prototype[property], property + " function is not the same as Array one");
+ }.bind(this));
+ }.bind(this), "Testing value iterable interface " + this.name);
+ }
+};
+
+IdlInterface.prototype.test_member_stringifier = function(member)
+{
+ subsetTestByKey(this.name, test, function()
+ {
+ if (this.is_callback() && !this.has_constants()) {
+ 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(new 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(new TypeError(), function() {
+ interfacePrototypeObject.toString.apply({}, []);
+ }, "calling stringifier with this = {} didn't throw TypeError");
+ }.bind(this), this.name + " interface: stringifier");
+};
+
+IdlInterface.prototype.test_members = function()
+{
+ for (var i = 0; i < this.members.length; i++)
+ {
+ var member = this.members[i];
+ switch (member.type) {
+ case "iterable":
+ this.add_iterable_members(member);
+ break;
+ // TODO: add setlike and maplike handling.
+ default:
+ break;
+ }
+ }
+
+ 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))) {
+ 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.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.stringifier) {
+ this.test_member_stringifier(member);
+ }
+ break;
+
+ case "iterable":
+ this.test_member_iterable(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 =
+ this.members.some(function(member) { return member.legacycaller; })
+ ? "function"
+ : "object";
+
+ 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://heycam.github.io/webidl/#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.has_extended_attribute("NoInterfaceObject")
+ && (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;
+ }
+
+ 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))) {
+ 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)
+ {
+ var described_name = member.name;
+ if (member.type == "operation")
+ {
+ described_name += "(" + member.arguments.map(arg => arg.idlType.idlType).join(", ") + ")";
+ }
+ 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["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)
+ {
+ 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 "' + described_name + '" 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 a_test = subsetTestByKey(this.name, async_test, this.name + " interface: calling " + member.name +
+ "(" + member.arguments.map(function(m) { return m.idlType.idlType; }).join(", ") +
+ ") on " + desc + " with too few arguments must throw TypeError");
+ 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["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(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.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 = [];
+
+ // "For each exposed attribute of the interface, whether it was declared on
+ // the interface itself or one of its consequential interfaces, there MUST
+ // exist a corresponding property. The characteristics of this property are
+ // as follows:"
+
+ // "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
+ // [Unforgeable] 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, "[Unforgeable] 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["static"]) {
+ // "If O is not a platform object that implements I, then:
+ // "If the attribute was specified with the [LenientThis] extended
+ // attribute, then return undefined.
+ // "Otherwise, throw a TypeError."
+ if (!member.has_extended_attribute("LenientThis")) {
+ if (member.idlType.generic !== "Promise") {
+ assert_throws(new TypeError(), function() {
+ desc.get.call({});
+ }.bind(this), "calling getter on wrong object type must throw TypeError");
+ } else {
+ pendingPromises.push(
+ promise_rejects(a_test, new 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("LenientSetter")
+ && !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["static"]) {
+ // "If /validThis/ is false and the attribute was not specified
+ // with the [LenientThis] 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("LenientThis")) {
+ assert_throws(new 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)
+ {
+ this[k] = obj[k];
+ }
+ if (!("extAttrs" in this))
+ {
+ this.extAttrs = [];
+ }
+
+ this.isUnforgeable = this.has_extended_attribute("Unforgeable");
+ this.isUnscopable = this.has_extended_attribute("Unscopable");
+}
+
+IdlInterfaceMember.prototype = Object.create(IdlObject.prototype);
+
+IdlInterfaceMember.prototype.is_to_json_regular_operation = function() {
+ return this.type == "operation" && !this.static && this.name == "toJSON";
+};
+
+/// 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);
+
+/// 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 args = member.arguments.map(function(a) {
+ var s = a.idlType.idlType;
+ if (a.variadic) {
+ s += '...';
+ }
+ return s;
+ }).join(", ");
+ var a_test = subsetTestByKey(
+ this.name,
+ async_test,
+ this.name + ' namespace: operation ' + member.name + '(' + args + ')');
+ 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 = function ()
+{
+ /**
+ * TODO(lukebjerring): Assert:
+ * - "Note that unlike interfaces or dictionaries, namespaces do not create types."
+ * - "Of the extended attributes defined in this specification, only the
+ * [Exposed] and [SecureContext] extended attributes are applicable to namespaces."
+ * - "Namespaces must be annotated with the [Exposed] extended attribute."
+ */
+
+ 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|String[]} srcs Spec name(s) for source idl files (fetched from
+ * /interfaces/{name}.idl).
+ * @param {String|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();
+ srcs = (srcs instanceof Array) ? srcs : [srcs] || [];
+ deps = (deps instanceof Array) ? deps : [deps] || [];
+ var setup_error = null;
+ return Promise.all(
+ srcs.concat(deps).map(function(spec) {
+ return fetch_spec(spec);
+ }))
+ .then(function(idls) {
+ for (var i = 0; i < srcs.length; i++) {
+ idl_array.add_idls(idls[i]);
+ }
+ for (var i = srcs.length; i < srcs.length + deps.length; i++) {
+ idl_array.add_dependency_idls(idls[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();
+ });
+}
+// vim: set expandtab shiftwidth=4 tabstop=4 foldmarker=@{,@} foldmethod=marker:
diff --git a/test/fixtures/wpt/resources/idlharness.js.headers b/test/fixtures/wpt/resources/idlharness.js.headers
new file mode 100644
index 00000000000000..5e8f640c6659d1
--- /dev/null
+++ b/test/fixtures/wpt/resources/idlharness.js.headers
@@ -0,0 +1,2 @@
+Content-Type: text/javascript; charset=utf-8
+Cache-Control: max-age=3600
diff --git a/test/fixtures/wpt/resources/readme.md b/test/fixtures/wpt/resources/readme.md
new file mode 100644
index 00000000000000..97d2e6f92203d1
--- /dev/null
+++ b/test/fixtures/wpt/resources/readme.md
@@ -0,0 +1,26 @@
+# Resources
+
+## `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.
+
+### Getting started
+
+To use `testharness.js` you must include two scripts, in the order given:
+
+``` html
+
+
+```
+
+### Full documentation
+
+For detailed API documentation please visit [https://web-platform-tests.org/writing-tests/testharness-api.html](https://web-platform-tests.org/writing-tests/testharness-api.html).
+
+### Tutorials
+
+You can also read a tutorial on
+[Using testharness.js](http://darobin.github.com/test-harness-tutorial/docs/using-testharness.html).
diff --git a/test/fixtures/wpt/resources/sriharness.js b/test/fixtures/wpt/resources/sriharness.js
new file mode 100644
index 00000000000000..9d7fa76a7d65f6
--- /dev/null
+++ b/test/fixtures/wpt/resources/sriharness.js
@@ -0,0 +1,100 @@
+var SRIScriptTest = function(pass, name, src, integrityValue, crossoriginValue, nonce) {
+ this.pass = pass;
+ this.name = "Script: " + name;
+ this.src = src;
+ this.integrityValue = integrityValue;
+ this.crossoriginValue = crossoriginValue;
+ this.nonce = nonce;
+}
+
+SRIScriptTest.prototype.execute = function() {
+ var test = async_test(this.name);
+ var e = document.createElement("script");
+ e.src = this.src;
+ 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);
+};
+
+// 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");
+ 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/test/fixtures/wpt/resources/testdriver-actions.js b/test/fixtures/wpt/resources/testdriver-actions.js
new file mode 100644
index 00000000000000..46c68858e45746
--- /dev/null
+++ b/test/fixtures/wpt/resources/testdriver-actions.js
@@ -0,0 +1,391 @@
+(function() {
+ let sourceNameIdx = 0;
+
+ /**
+ * Builder for creating a sequence of actions
+ */
+ function Actions() {
+ this.sourceTypes = new Map([["key", KeySource],
+ ["pointer", PointerSource],
+ ["general", 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("general");
+ this.tickIdx = 0;
+ }
+
+ Actions.prototype = {
+ /**
+ * 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);
+ 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);
+ },
+
+ /**
+ * 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 ('general', 'key', or 'pointer')
+ * @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, true);
+ 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 key source
+ * @param {String} pointerType - Type of pointing device
+ * @param {Bool} set - Set source as the default key source
+ * @returns {Actions}
+ */
+ addPointer: function(name, pointerType="mouse", set=true) {
+ this.createSource("pointer", name, true, {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;
+ },
+
+ 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.
+ * @returns {Actions}
+ */
+ pause: function(duration) {
+ this.getSource("general").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=0, sourceName=null}={}) {
+ let source = this.getSource("pointer", sourceName);
+ source.pointerDown(this, button);
+ 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=0, 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}={}) {
+ let source = this.getSource("pointer", sourceName);
+ source.pointerMove(this, x, y, duration, origin);
+ return this;
+ },
+ };
+
+ function GeneralSource() {
+ this.actions = new Map();
+ }
+
+ GeneralSource.prototype = {
+ serialize: function(tickCount) {
+ if (!this.actions.size) {
+ return undefined;
+ }
+ let actions = [];
+ let data = {"type": "none", "actions": actions};
+ for (let i=0; i 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) {
+ this_obj._dispatch({
+ type: "complete",
+ tests: map(tests,
+ function(test) {
+ return test.structured_clone();
+ }),
+ status: harness_status.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") {
+ if (event.ports && event.ports[0]) {
+ // If a MessageChannel was passed, then use it to
+ // send results back to the main window. This
+ // allows the tests to work even if the browser
+ // does not fully support MessageEvent.source in
+ // ServiceWorkers yet.
+ this_obj._add_message_port(event.ports[0]);
+ event.ports[0].start();
+ } else {
+ // If there is no MessageChannel, then attempt to
+ // use the MessageEvent.source to send results
+ // back to the main window.
+ 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;
+ }
+ };
+
+ /*
+ * 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 "Untitled" + 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();
+ }
+
+ if (!('self' in global_scope)) {
+ return new ShellTestEnvironment();
+ }
+
+ throw new Error("Unsupported test environment");
+ }
+
+ 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]';
+ }
+
+ /*
+ * API functions
+ */
+ function test(func, name, properties)
+ {
+ var test_name = name ? name : test_environment.next_default_test_name();
+ properties = properties ? properties : {};
+ 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 +
+ "\" inappropriately returned a value";
+
+ try {
+ if (value && value.hasOwnProperty("then")) {
+ msg += ", consider using `promise_test` instead";
+ }
+ } catch (err) {}
+
+ tests.status.status = tests.status.ERROR;
+ tests.status.message = msg;
+ }
+
+ if (test_obj.phase === test_obj.phases.STARTED) {
+ test_obj.done();
+ }
+ }
+
+ function async_test(func, name, properties)
+ {
+ if (typeof func !== "function") {
+ properties = name;
+ name = func;
+ func = null;
+ }
+ var test_name = name ? name : test_environment.next_default_test_name();
+ properties = properties ? properties : {};
+ var test_obj = new Test(test_name, properties);
+ if (func) {
+ test_obj.step(func, test_obj, test_obj);
+ }
+ return test_obj;
+ }
+
+ function promise_test(func, name, properties) {
+ var test = async_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();
+ });
+ });
+ });
+ }
+
+ function promise_rejects(test, expected, promise, description) {
+ return promise.then(test.unreached_func("Should have rejected: " + description)).catch(function(e) {
+ assert_throws(expected, function() { throw e }, description);
+ });
+ }
+
+ /**
+ * This constructor helper allows DOM events to be handled using Promises,
+ * which 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.
+ */
+ function EventWatcher(test, watchedNode, eventTypes)
+ {
+ 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 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.
+ *
+ * For example,
+ *
+ * ```js
+ * 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) {
+ waitingFor = {
+ types: types,
+ resolve: resolve,
+ reject: reject
+ };
+ });
+ };
+
+ 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');
+
+ 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);
+ }
+
+ function done() {
+ if (tests.tests.length === 0) {
+ tests.set_file_is_test();
+ }
+ 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();
+ }
+
+ 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);
+ });
+ }
+
+ function on_event(object, event, callback)
+ {
+ object.addEventListener(event, callback, false);
+ }
+
+ function step_timeout(f, t) {
+ var outer_this = this;
+ var args = Array.prototype.slice.call(arguments, 2);
+ return setTimeout(function() {
+ f.apply(outer_this, args);
+ }, t * tests.timeout_multiplier);
+ }
+
+ expose(test, 'test');
+ expose(async_test, 'async_test');
+ expose(promise_test, 'promise_test');
+ expose(promise_rejects, 'promise_rejects');
+ expose(generate_tests, 'generate_tests');
+ expose(setup, '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
+ */
+ 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)) {
+ return "[" + val.map(function(x) {return format_value(x, seen);}).join(", ") + "]";
+ }
+
+ switch (typeof val) {
+ case "string":
+ val = val.replace("\\", "\\\\");
+ 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 ";
+ 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 assert_true(actual, description)
+ {
+ assert(actual === true, "assert_true", description,
+ "expected true got ${actual}", {actual:actual});
+ }
+ expose(assert_true, "assert_true");
+
+ function assert_false(actual, description)
+ {
+ assert(actual === false, "assert_false", description,
+ "expected false got ${actual}", {actual:actual});
+ }
+ expose(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;
+ }
+
+ 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_equals, "assert_equals");
+
+ function assert_not_equals(actual, expected, description)
+ {
+ /*
+ * Test if two primitives are unequal or two objects
+ * are different objects
+ */
+ assert(!same_value(actual, expected), "assert_not_equals", description,
+ "got disallowed value ${actual}",
+ {actual:actual});
+ }
+ expose(assert_not_equals, "assert_not_equals");
+
+ 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_in_array, "assert_in_array");
+
+ 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, actual:actual});
+ }
+ }
+ 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_object_equals, "assert_object_equals");
+
+ function assert_array_equals(actual, expected, description)
+ {
+ 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 ${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_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(same_value(expected[i], actual[i]),
+ "assert_array_equals", description,
+ "property ${i}, expected ${expected} but got ${actual}",
+ {i:i, expected:expected[i], actual:actual[i]});
+ }
+ }
+ expose(assert_array_equals, "assert_array_equals");
+
+ 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]});
+ }
+ }
+ expose(assert_array_approx_equals, "assert_array_approx_equals");
+
+ 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});
+
+ assert(Math.abs(actual - expected) <= epsilon,
+ "assert_approx_equals", description,
+ "expected ${expected} +/- ${epsilon} but got ${actual}",
+ {expected:expected, actual:actual, epsilon:epsilon});
+ }
+ expose(assert_approx_equals, "assert_approx_equals");
+
+ 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_less_than, "assert_less_than");
+
+ 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_greater_than, "assert_greater_than");
+
+ 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_between_exclusive, "assert_between_exclusive");
+
+ 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_less_than_equal, "assert_less_than_equal");
+
+ 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_greater_than_equal, "assert_greater_than_equal");
+
+ 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_between_inclusive, "assert_between_inclusive");
+
+ 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_regexp_match, "assert_regexp_match");
+
+ function assert_class_string(object, class_string, description) {
+ assert_equals({}.toString.call(object), "[object " + class_string + "]",
+ description);
+ }
+ expose(assert_class_string, "assert_class_string");
+
+
+ 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_own_property, "assert_own_property");
+
+ 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_not_own_property, "assert_not_own_property");
+
+ function _assert_inherits(name) {
+ return function (object, property_name, description)
+ {
+ assert(typeof object === "object" || typeof object === "function",
+ 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});
+ };
+ }
+ expose(_assert_inherits("assert_inherits"), "assert_inherits");
+ expose(_assert_inherits("assert_idl_attribute"), "assert_idl_attribute");
+
+ 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_readonly, "assert_readonly");
+
+ /**
+ * Assert an Exception with the expected code is thrown.
+ *
+ * @param {object|number|string} code The expected exception code.
+ * @param {Function} func Function which should throw.
+ * @param {string} description Error description for the case that the error is not thrown.
+ */
+ function assert_throws(code, func, description)
+ {
+ try {
+ func.call(this);
+ assert(false, "assert_throws", description,
+ "${func} did not throw", {func:func});
+ } catch (e) {
+ if (e instanceof AssertionError) {
+ throw e;
+ }
+
+ assert(typeof e === "object",
+ "assert_throws", description,
+ "${func} threw ${e} with type ${type}, not an object",
+ {func:func, e:e, type:typeof e});
+
+ assert(e !== null,
+ "assert_throws", description,
+ "${func} threw null, not an object",
+ {func:func});
+
+ if (code === null) {
+ throw new AssertionError('Test bug: need to pass exception to assert_throws()');
+ }
+ if (typeof code === "object") {
+ assert("name" in e && e.name == code.name,
+ "assert_throws", description,
+ "${func} threw ${actual} (${actual_name}) expected ${expected} (${expected_name})",
+ {func:func, actual:e, actual_name:e.name,
+ expected:code,
+ expected_name:code.name});
+ return;
+ }
+
+ var code_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 in code_name_map ? code_name_map[code] : code;
+
+ 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
+ };
+
+ if (!(name in name_code_map)) {
+ throw new AssertionError('Test bug: unrecognized DOMException code "' + code + '" passed to assert_throws()');
+ }
+
+ var 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;
+ }
+
+ //We'd like to test that e instanceof the appropriate interface,
+ //but we can't, because we don't know what window it was created
+ //in. It might be an instanceof the appropriate interface on some
+ //unknown other window. TODO: Work around this somehow?
+
+ for (var prop in required_props) {
+ assert(prop in e && e[prop] == required_props[prop],
+ "assert_throws", description,
+ "${func} threw ${e} that is not a DOMException " + code + ": property ${prop} is equal to ${actual}, expected ${expected}",
+ {func:func, e:e, prop:prop, actual:e[prop], expected:required_props[prop]});
+ }
+ }
+ }
+ expose(assert_throws, "assert_throws");
+
+ function assert_unreached(description) {
+ assert(false, "assert_unreached", description,
+ "Reached unreachable code");
+ }
+ expose(assert_unreached, "assert_unreached");
+
+ function assert_any(assert_func, actual, expected_array)
+ {
+ var args = [].slice.call(arguments, 3);
+ 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"));
+ }
+ }
+ expose(assert_any, "assert_any");
+
+ function Test(name, properties)
+ {
+ if (tests.file_is_test && tests.tests.length) {
+ throw new Error("Tried to create a test with file_is_test");
+ }
+ this.name = name;
+
+ this.phase = tests.is_aborted ?
+ this.phases.COMPLETE : this.phases.INITIAL;
+
+ this.status = this.NOTRUN;
+ this.timeout_id = null;
+ this.index = null;
+
+ this.properties = properties;
+ var timeout = properties.timeout ? properties.timeout : settings.test_timeout;
+ if (timeout !== null) {
+ this.timeout_length = timeout * tests.timeout_multiplier;
+ } else {
+ this.timeout_length = null;
+ }
+
+ this.message = null;
+ this.stack = null;
+
+ this.steps = [];
+ this._is_promise_test = false;
+
+ this.cleanup_callbacks = [];
+ this._user_defined_cleanup_count = 0;
+ this._done_callbacks = [];
+
+ tests.push(this);
+ }
+
+ Test.statuses = {
+ PASS:0,
+ FAIL:1,
+ TIMEOUT:2,
+ NOTRUN:3
+ };
+
+ Test.prototype = merge({}, Test.statuses);
+
+ Test.prototype.phases = {
+ INITIAL:0,
+ STARTED:1,
+ HAS_RESULT:2,
+ CLEANING:3,
+ COMPLETE:4
+ };
+
+ 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;
+ };
+
+ Test.prototype.step = function(func, this_obj)
+ {
+ if (this.phase > this.phases.STARTED) {
+ return;
+ }
+ 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.notify_test_state(this);
+
+ if (this.timeout_id === null) {
+ this.set_timeout();
+ }
+
+ this.steps.push(func);
+
+ if (arguments.length === 1) {
+ this_obj = this;
+ }
+
+ try {
+ return func.apply(this_obj, Array.prototype.slice.call(arguments, 2));
+ } catch (e) {
+ if (this.phase >= this.phases.HAS_RESULT) {
+ return;
+ }
+ var message = String((typeof e === "object" && e !== null) ? e.message : e);
+ var stack = e.stack ? e.stack : null;
+
+ this.set_status(this.FAIL, message, stack);
+ this.phase = this.phases.HAS_RESULT;
+ this.done();
+ }
+ };
+
+ 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)));
+ };
+ };
+
+ 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();
+ };
+ };
+
+ Test.prototype.unreached_func = function(description)
+ {
+ return this.step_func(function() {
+ assert_unreached(description);
+ });
+ };
+
+ Test.prototype.step_timeout = function(f, timeout) {
+ var test_this = this;
+ var args = Array.prototype.slice.call(arguments, 2);
+ return setTimeout(this.step_func(function() {
+ return f.apply(test_this, args);
+ }), timeout * tests.timeout_multiplier);
+ }
+
+ /*
+ * 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.
+ */
+ 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;
+ };
+
+ Test.prototype.timeout = function()
+ {
+ this.timeout_id = null;
+ this.set_status(this.TIMEOUT, "Test timed out");
+ this.phase = this.phases.HAS_RESULT;
+ this.done();
+ };
+
+ Test.prototype.force_timeout = Test.prototype.timeout;
+
+ /**
+ * Update the test status, initiate "cleanup" functions, and signal test
+ * completion.
+ */
+ 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);
+ }
+
+ 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 error_count = 0;
+ var bad_value_count = 0;
+ function on_error() {
+ error_count += 1;
+ // 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;
+
+ forEach(this.cleanup_callbacks,
+ function(cleanup_callback) {
+ var result;
+
+ try {
+ result = cleanup_callback();
+ } catch (e) {
+ on_error();
+ 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, error_count, 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, error_count, 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, error_count, bad_value_count) {
+ if (error_count || bad_value_count) {
+ var total = test._user_defined_cleanup_count;
+
+ tests.status.status = tests.status.ERROR;
+ tests.status.message = "Test named '" + test.name +
+ "' specified " + total +
+ " 'cleanup' function" + (total > 1 ? "s" : "");
+
+ if (error_count) {
+ tests.status.message += ", and " + error_count + " failed";
+ }
+
+ 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 += ".";
+
+ tests.status.stack = null;
+ }
+
+ test.phase = test.phases.COMPLETE;
+ tests.result(test);
+ forEach(test._done_callbacks,
+ function(callback) {
+ callback();
+ });
+ test._done_callbacks.length = 0;
+ }
+
+ /*
+ * 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();
+ });
+ }
+
+ /*
+ * 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.tests = new Array();
+
+ 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) {
+ 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);
+
+ if (error.preventDefault) {
+ error.preventDefault();
+ }
+ };
+
+ 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.sack);
+ }
+
+ 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 = {
+ test_state: RemoteContext.prototype.test_state,
+ result: RemoteContext.prototype.test_done,
+ complete: RemoteContext.prototype.remote_done
+ };
+
+ /*
+ * Harness
+ */
+
+ function TestsStatus()
+ {
+ this.status = null;
+ this.message = null;
+ this.stack = null;
+ }
+
+ TestsStatus.statuses = {
+ OK:0,
+ ERROR:1,
+ TIMEOUT:2
+ };
+
+ TestsStatus.prototype = merge({}, TestsStatus.statuses);
+
+ 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;
+ };
+
+ 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.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.pending_remotes = [];
+
+ 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 == "timeout_multiplier") {
+ this.timeout_multiplier = value;
+ }
+ }
+ }
+
+ if (func) {
+ try {
+ func();
+ } catch (e) {
+ this.status.status = this.status.ERROR;
+ this.status.message = String(e);
+ this.status.stack = e.stack ? e.stack : null;
+ }
+ }
+ 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
+ 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 && 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);
+ };
+
+ /**
+ * 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;
+ };
+
+ Tests.prototype.notify_complete = function() {
+ var this_obj = this;
+ var duplicates;
+
+ if (this.status.status === null) {
+ duplicates = this.find_duplicates();
+
+ // 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);
+ });
+ };
+
+ /*
+ * 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)) {
+ if (window.MessageChannel) {
+ // The ServiceWorker's implicit MessagePort is currently not
+ // reliably accessible from the ServiceWorkerGlobalScope due to
+ // Blink setting MessageEvent.source to null for messages sent
+ // via ServiceWorker.postMessage(). Until that's resolved,
+ // create an explicit MessageChannel and pass one end to the
+ // worker.
+ var message_channel = new MessageChannel();
+ message_port = message_channel.port1;
+ message_port.start();
+ worker.postMessage({type: "connect"}, [message_channel.port2]);
+ } else {
+ // If MessageChannel is not available, then try the
+ // ServiceWorker.postMessage() approach using MessageEvent.source
+ // on the other end.
+ 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;
+ };
+
+ 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;
+ }
+
+ this.pending_remotes.push(this.create_remote_window(remote));
+ };
+
+ function fetch_tests_from_window(window) {
+ tests.fetch_tests_from_window(window);
+ }
+ expose(fetch_tests_from_window, 'fetch_tests_from_window');
+
+ function timeout() {
+ if (tests.timeout_length === null) {
+ tests.timeout();
+ }
+ }
+ expose(timeout, 'timeout');
+
+ function add_start_callback(callback) {
+ tests.start_callbacks.push(callback);
+ }
+
+ function add_test_state_callback(callback) {
+ tests.test_state_callbacks.push(callback);
+ }
+
+ function add_result_callback(callback) {
+ tests.test_done_callbacks.push(callback);
+ }
+
+ 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 (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 (!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) {
+ return;
+ }
+ if (this.phase < this.HAVE_RESULTS) {
+ this.resolve_log();
+ this.phase = this.HAVE_RESULTS;
+ }
+ var done_count = tests.tests.length - tests.num_pending;
+ if (this.output_node) {
+ 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) {
+ 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_text_harness = {};
+ status_text_harness[harness_status.OK] = "OK";
+ status_text_harness[harness_status.ERROR] = "Error";
+ status_text_harness[harness_status.TIMEOUT] = "Timeout";
+
+ var status_text = {};
+ status_text[Test.prototype.PASS] = "Pass";
+ status_text[Test.prototype.FAIL] = "Fail";
+ status_text[Test.prototype.TIMEOUT] = "Timeout";
+ status_text[Test.prototype.NOTRUN] = "Not Run";
+
+ var status_number = {};
+ forEach(tests,
+ function(test) {
+ var status = status_text[test.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 = status_text_harness[harness_status.status];
+ var rv = [["section", {},
+ ["p", {},
+ "Harness status: ",
+ ["span", {"class":status_class(status)},
+ status
+ ],
+ ]
+ ]];
+
+ 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 (status_text.hasOwnProperty(i)) {
+ if (status_number.hasOwnProperty(status_text[i])) {
+ var status = status_text[i];
+ rv[0].push(["div", {"class":status_class(status)},
+ ["label", {},
+ ["input", {type:"checkbox", checked:"checked"}],
+ status_number[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.parentNode.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."+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
adds tens of seconds of execution time for
+ // large test suites (tens of thousands of tests).
+ function escape_html(s)
+ {
+ return s.replace(/\&/g, "&")
+ .replace(/Details
" +
+ "
Result
Test Name
" +
+ (assertions ? "
Assertion
" : "") +
+ "
Message
" +
+ "";
+ for (var i = 0; i < tests.length; i++) {
+ html += '
";
+ 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 (tests.tests.length === 0) {
+ tests.set_file_is_test();
+ }
+ if (expected_true !== true) {
+ var msg = make_message(function_name, description,
+ error, substitutions);
+ throw new AssertionError(msg);
+ }
+ }
+
+ function AssertionError(message)
+ {
+ this.message = message;
+ this.stack = this.get_stack();
+ }
+ expose(AssertionError, "AssertionError");
+
+ AssertionError.prototype = Object.create(Error.prototype);
+
+ AssertionError.prototype.get_stack = function() {
+ var stack = new Error().stack;
+ // IE11 does not initialize 'Error.stack' until the object is thrown.
+ if (!stack) {
+ try {
+ throw new Error();
+ } catch (e) {
+ stack = e.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 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 provided `values`. Two
+ * arguments will be available in each
+ * invocation: the value from `values` 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
+```
+
+## Documentation
+
+The API to WebIDL2 is trivial: you parse a string of WebIDL and it returns a syntax tree.
+
+### Parsing
+
+In Node, that happens with:
+
+```JS
+var WebIDL2 = require("webidl2");
+var tree = WebIDL2.parse("string of WebIDL");
+```
+
+In the browser:
+```HTML
+
+
+```
+
+### Errors
+
+When there is a syntax error in the WebIDL, it throws an exception object with the following
+properties:
+
+* `message`: the error message
+* `line`: the line at which the error occurred.
+* `input`: a short peek at the text at the point where the error happened
+* `tokens`: the five tokens at the point of error, as understood by the tokeniser
+ (this is the same content as `input`, but seen from the tokeniser's point of view)
+
+The exception also has a `toString()` method that hopefully should produce a decent
+error message.
+
+### AST (Abstract Syntax Tree)
+
+The `parse()` method returns a tree object representing the parse tree of the IDL.
+Comment and white space are not represented in the AST.
+
+The root of this object is always an array of definitions (where definitions are
+any of interfaces, dictionaries, callbacks, etc. — anything that can occur at the root
+of the IDL).
+
+### IDL Type
+
+This structure is used in many other places (operation return types, argument types, etc.).
+It captures a WebIDL type with a number of options. Types look like this and are typically
+attached to a field called `idlType`:
+
+```JS
+{
+ "type": "attribute-type",
+ "generic": null,
+ "idlType": "unsigned short",
+ "nullable": false,
+ "union": false,
+ "extAttrs": [...]
+}
+```
+
+Where the fields are as follows:
+
+* `type`: String indicating where this type is used. Can be `null` if not applicable.
+* `generic`: String indicating the generic type (e.g. "Promise", "sequence"). `null`
+ otherwise.
+* `idlType`: Can be different things depending on context. In most cases, this will just
+ be a string with the type name. But the reason this field isn't called "typeName" is
+ because it can take more complex values. If the type is a union, then this contains an
+ array of the types it unites. If it is a generic type, it contains the IDL type
+ description for the type in the sequence, the eventual value of the promise, etc.
+* `nullable`: Boolean indicating whether this is nullable or not.
+* `union`: Boolean indicating whether this is a union type or not.
+* `extAttrs`: A list of [extended attributes](#extended-attributes).
+
+### Interface
+
+Interfaces look like this:
+
+```JS
+{
+ "type": "interface",
+ "name": "Animal",
+ "partial": false,
+ "members": [...],
+ "inheritance": null,
+ "extAttrs": [...]
+}, {
+ "type": "interface",
+ "name": "Human",
+ "partial": false,
+ "members": [...],
+ "inheritance": "Animal",
+ "extAttrs": [...]
+}
+```
+
+The fields are as follows:
+
+* `type`: Always "interface".
+* `name`: The name of the interface.
+* `partial`: A boolean indicating whether it's a partial interface.
+* `members`: An array of interface members (attributes, operations, etc.). Empty if there are none.
+* `inheritance`: A string giving the name of an interface this one inherits from, `null` otherwise.
+ **NOTE**: In v1 this was an array, but multiple inheritance is no longer supported so this didn't make
+ sense.
+* `extAttrs`: A list of [extended attributes](#extended-attributes).
+
+### Interface mixins
+
+Interfaces mixins look like this:
+
+```JS
+{
+ "type": "interface mixin",
+ "name": "Animal",
+ "partial": false,
+ "members": [...],
+ "extAttrs": [...]
+}, {
+ "type": "interface mixin",
+ "name": "Human",
+ "partial": false,
+ "members": [...],
+ "extAttrs": [...]
+}
+```
+
+The fields are as follows:
+
+* `type`: Always "interface mixin".
+* `name`: The name of the interface mixin.
+* `partial`: A boolean indicating whether it's a partial interface mixin.
+* `members`: An array of interface members (attributes, operations, etc.). Empty if there are none.
+* `extAttrs`: A list of [extended attributes](#extended-attributes).
+
+### Namespace
+
+Namespaces look like this:
+
+```JS
+{
+ "type": "namespace",
+ "name": "Console",
+ "partial": false,
+ "members": [...],
+ "extAttrs": [...]
+}
+```
+
+The fields are as follows:
+
+* `type`: Always "namespace".
+* `name`: The name of the namespace.
+* `partial`: A boolean indicating whether it's a partial namespace.
+* `members`: An array of namespace members (attributes and operations). Empty if there are none.
+* `extAttrs`: A list of [extended attributes](#extended-attributes).
+
+### Callback Interfaces
+
+These are captured by the same structure as [Interfaces](#interface) except that
+their `type` field is "callback interface".
+
+### Callback
+
+A callback looks like this:
+
+```JS
+{
+ "type": "callback",
+ "name": "AsyncOperationCallback",
+ "idlType": {
+ "type": "return-type",
+ "sequence": false,
+ "generic": null,
+ "nullable": false,
+ "union": false,
+ "idlType": "void",
+ "extAttrs": []
+ },
+ "arguments": [...],
+ "extAttrs": []
+}
+```
+
+The fields are as follows:
+
+* `type`: Always "callback".
+* `name`: The name of the callback.
+* `idlType`: An [IDL Type](#idl-type) describing what the callback returns.
+* `arguments`: A list of [arguments](#arguments), as in function paramters.
+* `extAttrs`: A list of [extended attributes](#extended-attributes).
+
+### Dictionary
+
+A dictionary looks like this:
+
+```JS
+{
+ "type": "dictionary",
+ "name": "PaintOptions",
+ "partial": false,
+ "members": [{
+ "type": "field",
+ "name": "fillPattern",
+ "required": false,
+ "idlType": {
+ "type": "dictionary-type",
+ "sequence": false,
+ "generic": null,
+ "nullable": true,
+ "union": false,
+ "idlType": "DOMString",
+ "extAttrs": [...]
+ },
+ "extAttrs": [],
+ "default": {
+ "type": "string",
+ "value": "black"
+ }
+ }],
+ "inheritance": null,
+ "extAttrs": []
+}
+```
+
+The fields are as follows:
+
+* `type`: Always "dictionary".
+* `name`: The dictionary name.
+* `partial`: Boolean indicating whether it's a partial dictionary.
+* `members`: An array of members (see below).
+* `inheritance`: A string indicating which dictionary is being inherited from, `null` otherwise.
+* `extAttrs`: A list of [extended attributes](#extended-attributes).
+
+All the members are fields as follows:
+
+* `type`: Always "field".
+* `name`: The name of the field.
+* `required`: Boolean indicating whether this is a [required](https://heycam.github.io/webidl/#required-dictionary-member) field.
+* `idlType`: An [IDL Type](#idl-type) describing what field's type.
+* `extAttrs`: A list of [extended attributes](#extended-attributes).
+* `default`: A [default value](#default-and-const-values), absent if there is none.
+
+### Enum
+
+An enum looks like this:
+
+```JS
+{
+ "type": "enum",
+ "name": "MealType",
+ "values": [
+ { "type": "string", "value": "rice" },
+ { "type": "string", "value": "noodles" },
+ { "type": "string", "value": "other" }
+ ],
+ "extAttrs": []
+}
+```
+
+The fields are as follows:
+
+* `type`: Always "enum".
+* `name`: The enum's name.
+* `values`: An array of values.
+* `extAttrs`: A list of [extended attributes](#extended-attributes).
+
+### Typedef
+
+A typedef looks like this:
+
+```JS
+{
+ "type": "typedef",
+ "idlType": {
+ "type": "typedef-type",
+ "sequence": true,
+ "generic": "sequence",
+ "nullable": false,
+ "union": false,
+ "idlType": {
+ "type": "typedef-type",
+ "sequence": false,
+ "generic": null,
+ "nullable": false,
+ "union": false,
+ "idlType": "Point",
+ "extAttrs": [...]
+ },
+ "extAttrs": [...]
+ },
+ "name": "PointSequence",
+ "extAttrs": []
+}
+```
+
+
+The fields are as follows:
+
+* `type`: Always "typedef".
+* `name`: The typedef's name.
+* `idlType`: An [IDL Type](#idl-type) describing what typedef's type.
+* `extAttrs`: A list of [extended attributes](#extended-attributes).
+
+### Implements
+
+An implements definition looks like this:
+
+```JS
+{
+ "type": "implements",
+ "target": "Node",
+ "implements": "EventTarget",
+ "extAttrs": []
+}
+```
+
+The fields are as follows:
+
+* `type`: Always "implements".
+* `target`: The interface that implements another.
+* `implements`: The interface that is being implemented by the target.
+* `extAttrs`: A list of [extended attributes](#extended-attributes).
+
+### Includes
+
+An includes definition looks like this:
+
+```JS
+{
+ "type": "includes",
+ "target": "Node",
+ "includes": "EventTarget",
+ "extAttrs": []
+}
+```
+
+The fields are as follows:
+
+* `type`: Always "includes".
+* `target`: The interface that includes an interface mixin.
+* `includes`: The interface mixin that is being included by the target.
+* `extAttrs`: A list of [extended attributes](#extended-attributes).
+
+### Operation Member
+
+An operation looks like this:
+```JS
+{
+ "type": "operation",
+ "getter": false,
+ "setter": false,
+ "deleter": false,
+ "static": false,
+ "stringifier": false,
+ "idlType": {
+ "type": "return-type",
+ "sequence": false,
+ "generic": null,
+ "nullable": false,
+ "union": false,
+ "idlType": "void",
+ "extAttrs": []
+ },
+ "name": "intersection",
+ "arguments": [{
+ "optional": false,
+ "variadic": true,
+ "extAttrs": [],
+ "idlType": {
+ "type": "argument-type",
+ "sequence": false,
+ "generic": null,
+ "nullable": false,
+ "union": false,
+ "idlType": "long",
+ "extAttrs": [...]
+ },
+ "name": "ints"
+ }],
+ "extAttrs": []
+}
+```
+
+The fields are as follows:
+
+* `type`: Always "operation".
+* `getter`: True if a getter operation.
+* `setter`: True if a setter operation.
+* `deleter`: True if a deleter operation.
+* `static`: True if a static operation.
+* `stringifier`: True if a stringifier operation.
+* `idlType`: An [IDL Type](#idl-type) of what the operation returns. If a stringifier, may be absent.
+* `name`: The name of the operation. If a stringifier, may be `null`.
+* `arguments`: An array of [arguments](#arguments) for the operation.
+* `extAttrs`: A list of [extended attributes](#extended-attributes).
+
+### Attribute Member
+
+An attribute member looks like this:
+
+```JS
+{
+ "type": "attribute",
+ "static": false,
+ "stringifier": false,
+ "inherit": false,
+ "readonly": false,
+ "idlType": {
+ "type": "attribute-type",
+ "sequence": false,
+ "generic": null,
+ "nullable": false,
+ "union": false,
+ "idlType": "RegExp",
+ "extAttrs": [...]
+ },
+ "name": "regexp",
+ "extAttrs": []
+}
+```
+
+The fields are as follows:
+
+* `type`: Always "attribute".
+* `name`: The attribute's name.
+* `static`: True if it's a static attribute.
+* `stringifier`: True if it's a stringifier attribute.
+* `inherit`: True if it's an inherit attribute.
+* `readonly`: True if it's a read-only attribute.
+* `idlType`: An [IDL Type](#idl-type) for the attribute.
+* `extAttrs`: A list of [extended attributes](#extended-attributes).
+
+### Constant Member
+
+A constant member looks like this:
+
+```JS
+{
+ "type": "const",
+ "nullable": false,
+ "idlType": {
+ "type": "const-type",
+ "sequence": false,
+ "generic": null,
+ "nullable": false,
+ "union": false,
+ "idlType": "boolean"
+ "extAttrs": []
+ },
+ "name": "DEBUG",
+ "value": {
+ "type": "boolean",
+ "value": false
+ },
+ "extAttrs": []
+}
+```
+
+The fields are as follows:
+
+* `type`: Always "const".
+* `nullable`: Whether its type is nullable.
+* `idlType`: An [IDL Type](#idl-type) of the constant that represents a simple type, the type name.
+* `name`: The name of the constant.
+* `value`: The constant value as described by [Const Values](#default-and-const-values)
+* `extAttrs`: A list of [extended attributes](#extended-attributes).
+
+### Arguments
+
+The arguments (e.g. for an operation) look like this:
+
+```JS
+{
+ "arguments": [{
+ "optional": false,
+ "variadic": true,
+ "extAttrs": [],
+ "idlType": {
+ "type": "argument-type",
+ "sequence": false,
+ "generic": null,
+ "nullable": false,
+ "union": false,
+ "idlType": "long",
+ "extAttrs": [...]
+ },
+ "name": "ints"
+ }]
+}
+```
+
+The fields are as follows:
+
+* `optional`: True if the argument is optional.
+* `variadic`: True if the argument is variadic.
+* `idlType`: An [IDL Type](#idl-type) describing the type of the argument.
+* `name`: The argument's name.
+* `extAttrs`: A list of [extended attributes](#extended-attributes).
+
+### Extended Attributes
+
+Extended attributes are arrays of items that look like this:
+
+```JS
+{
+ "extAttrs": [{
+ "name": "TreatNullAs",
+ "arguments": null,
+ "type": "extended-attribute",
+ "rhs": {
+ "type": "identifier",
+ "value": "EmptyString"
+ }
+ }]
+}
+```
+
+The fields are as follows:
+
+* `name`: The extended attribute's name.
+* `arguments`: If the extended attribute takes arguments (e.g. `[Foo()]`) or if
+ its right-hand side does (e.g. `[NamedConstructor=Name(DOMString blah)]`) they
+ are listed here. Note that an empty arguments list will produce an empty array,
+ whereas the lack thereof will yield a `null`. If there is an `rhs` field then
+ they are the right-hand side's arguments, otherwise they apply to the extended
+ attribute directly.
+* `type`: Always `"extended-attribute"`.
+* `rhs`: If there is a right-hand side, this will capture its `type` (which can be
+ "identifier" or "identifier-list") and its `value`.
+
+### Default and Const Values
+
+Dictionary fields and operation arguments can take default values, and constants take
+values, all of which have the following fields:
+
+* `type`: One of string, number, boolean, null, Infinity, NaN, or sequence.
+
+For string, number, boolean, and sequence:
+
+* `value`: The value of the given type, as a string. For sequence, the only possible value is `[]`.
+
+For Infinity:
+
+* `negative`: Boolean indicating whether this is negative Infinity or not.
+
+### `iterable<>`, `legacyiterable<>`, `maplike<>`, `setlike<>` declarations
+
+These appear as members of interfaces that look like this:
+
+```JS
+{
+ "type": "maplike", // or "legacyiterable" / "iterable" / "setlike"
+ "idlType": /* One or two types */ ,
+ "readonly": false, // only for maplike and setlike
+ "extAttrs": []
+}
+```
+
+The fields are as follows:
+
+* `type`: Always one of "iterable", "legacyiterable", "maplike" or "setlike".
+* `idlType`: An array with one or more [IDL Types](#idl-type) representing the declared type arguments.
+* `readonly`: Whether the maplike or setlike is declared as read only.
+* `extAttrs`: A list of [extended attributes](#extended-attributes).
+
+
+## Testing
+
+### Running
+
+The test runs with mocha and expect.js. Normally, running mocha in the root directory
+should be enough once you're set up.
+
+### Coverage
+
+Current test coverage, as documented in `coverage.html`, is 95%. You can run your own
+coverage analysis with:
+
+```Bash
+jscoverage lib lib-cov
+```
+
+That will create the lib-cov directory with instrumented code; the test suite knows
+to use that if needed. You can then run the tests with:
+
+```Bash
+JSCOV=1 mocha --reporter html-cov > coverage.html
+```
+
+Note that I've been getting weirdly overescaped results from the html-cov reporter,
+so you might wish to try this instead:
+
+```Bash
+JSCOV=1 mocha --reporter html-cov | sed "s/<//g" | sed "s/"/\"/g" > coverage.html
+```
+### Browser tests
+
+In order to test in the browser, get inside `test/web` and run `make-web-tests.js`. This
+will generate a `browser-tests.html` file that you can open in a browser. As of this
+writing tests pass in the latest Firefox, Chrome, Opera, and Safari. Testing on IE
+and older versions will happen progressively.
diff --git a/test/fixtures/wpt/resources/webidl2/checker/index.html b/test/fixtures/wpt/resources/webidl2/checker/index.html
new file mode 100644
index 00000000000000..9897d8572f22a0
--- /dev/null
+++ b/test/fixtures/wpt/resources/webidl2/checker/index.html
@@ -0,0 +1,55 @@
+
+
+
+WebIDL 2 Checker
+
+
+
+
+
+
+
WebIDL Checker
+
This is an online checker for WebIDL built on the webidl2.js project.