summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/custom-elements
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /testing/web-platform/tests/custom-elements
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/custom-elements')
-rw-r--r--testing/web-platform/tests/custom-elements/CustomElementRegistry-constructor-and-callbacks-are-held-strongly.html84
-rw-r--r--testing/web-platform/tests/custom-elements/CustomElementRegistry.html755
-rw-r--r--testing/web-platform/tests/custom-elements/Document-createElement-customized-builtins.html104
-rw-r--r--testing/web-platform/tests/custom-elements/Document-createElement-svg.svg24
-rw-r--r--testing/web-platform/tests/custom-elements/Document-createElement.html344
-rw-r--r--testing/web-platform/tests/custom-elements/Document-createElementNS-customized-builtins.html43
-rw-r--r--testing/web-platform/tests/custom-elements/Document-createElementNS.html43
-rw-r--r--testing/web-platform/tests/custom-elements/ElementInternals-accessibility.html80
-rw-r--r--testing/web-platform/tests/custom-elements/HTMLElement-attachInternals.html82
-rw-r--r--testing/web-platform/tests/custom-elements/HTMLElement-constructor-customized-bulitins.html62
-rw-r--r--testing/web-platform/tests/custom-elements/HTMLElement-constructor.html186
-rw-r--r--testing/web-platform/tests/custom-elements/META.yml8
-rw-r--r--testing/web-platform/tests/custom-elements/adopted-callback.html167
-rw-r--r--testing/web-platform/tests/custom-elements/attribute-changed-callback.html271
-rw-r--r--testing/web-platform/tests/custom-elements/builtin-coverage.html182
-rw-r--r--testing/web-platform/tests/custom-elements/connected-callbacks-html-fragment-parsing.html49
-rw-r--r--testing/web-platform/tests/custom-elements/connected-callbacks-template.html33
-rw-r--r--testing/web-platform/tests/custom-elements/connected-callbacks.html88
-rw-r--r--testing/web-platform/tests/custom-elements/cross-realm-callback-report-exception.html83
-rw-r--r--testing/web-platform/tests/custom-elements/custom-element-reaction-queue.html190
-rw-r--r--testing/web-platform/tests/custom-elements/custom-element-registry/define-customized-builtins.html88
-rw-r--r--testing/web-platform/tests/custom-elements/custom-element-registry/define.html214
-rw-r--r--testing/web-platform/tests/custom-elements/custom-element-registry/per-global.html14
-rw-r--r--testing/web-platform/tests/custom-elements/custom-element-registry/upgrade.html157
-rw-r--r--testing/web-platform/tests/custom-elements/customized-built-in-constructor-exceptions.html107
-rw-r--r--testing/web-platform/tests/custom-elements/disconnected-callbacks.html93
-rw-r--r--testing/web-platform/tests/custom-elements/element-internals-shadowroot.html130
-rw-r--r--testing/web-platform/tests/custom-elements/enqueue-custom-element-callback-reactions-inside-another-callback.html223
-rw-r--r--testing/web-platform/tests/custom-elements/form-associated/ElementInternals-NotSupportedError.html24
-rw-r--r--testing/web-platform/tests/custom-elements/form-associated/ElementInternals-form.html61
-rw-r--r--testing/web-platform/tests/custom-elements/form-associated/ElementInternals-labels.html67
-rw-r--r--testing/web-platform/tests/custom-elements/form-associated/ElementInternals-reportValidity-bubble-ref.html28
-rw-r--r--testing/web-platform/tests/custom-elements/form-associated/ElementInternals-reportValidity-bubble.html29
-rw-r--r--testing/web-platform/tests/custom-elements/form-associated/ElementInternals-setFormValue.html587
-rw-r--r--testing/web-platform/tests/custom-elements/form-associated/ElementInternals-target-element-is-held-strongly.html26
-rw-r--r--testing/web-platform/tests/custom-elements/form-associated/ElementInternals-validation.html356
-rw-r--r--testing/web-platform/tests/custom-elements/form-associated/disabled-delegatesFocus.html56
-rw-r--r--testing/web-platform/tests/custom-elements/form-associated/fieldset-elements.html50
-rw-r--r--testing/web-platform/tests/custom-elements/form-associated/form-associated-callback.html249
-rw-r--r--testing/web-platform/tests/custom-elements/form-associated/form-disabled-callback.html136
-rw-r--r--testing/web-platform/tests/custom-elements/form-associated/form-elements-namedItem.html66
-rw-r--r--testing/web-platform/tests/custom-elements/form-associated/form-reset-callback.html58
-rw-r--r--testing/web-platform/tests/custom-elements/form-associated/label-delegatesFocus.html41
-rw-r--r--testing/web-platform/tests/custom-elements/historical.html35
-rw-r--r--testing/web-platform/tests/custom-elements/htmlconstructor/newtarget-customized-builtins.html116
-rw-r--r--testing/web-platform/tests/custom-elements/htmlconstructor/newtarget.html130
-rw-r--r--testing/web-platform/tests/custom-elements/microtasks-and-constructors.html123
-rw-r--r--testing/web-platform/tests/custom-elements/overwritten-customElements-global.html61
-rw-r--r--testing/web-platform/tests/custom-elements/parser/parser-constructs-custom-element-in-document-write.html43
-rw-r--r--testing/web-platform/tests/custom-elements/parser/parser-constructs-custom-element-synchronously.html51
-rw-r--r--testing/web-platform/tests/custom-elements/parser/parser-constructs-custom-elements-with-is.html51
-rw-r--r--testing/web-platform/tests/custom-elements/parser/parser-constructs-custom-elements.html48
-rw-r--r--testing/web-platform/tests/custom-elements/parser/parser-custom-element-in-foreign-content.html28
-rw-r--r--testing/web-platform/tests/custom-elements/parser/parser-fallsback-to-unknown-element.html91
-rw-r--r--testing/web-platform/tests/custom-elements/parser/parser-sets-attributes-and-children.html95
-rw-r--r--testing/web-platform/tests/custom-elements/parser/parser-uses-constructed-element.html75
-rw-r--r--testing/web-platform/tests/custom-elements/parser/parser-uses-create-an-element-for-a-token-svg.svg27
-rw-r--r--testing/web-platform/tests/custom-elements/parser/parser-uses-registry-of-owner-document.html154
-rw-r--r--testing/web-platform/tests/custom-elements/parser/serializing-html-fragments-customized-builtins.html37
-rw-r--r--testing/web-platform/tests/custom-elements/perform-microtask-checkpoint-before-construction-xml-parser.xhtml81
-rw-r--r--testing/web-platform/tests/custom-elements/perform-microtask-checkpoint-before-construction.html143
-rw-r--r--testing/web-platform/tests/custom-elements/prevent-extensions-crash.html15
-rw-r--r--testing/web-platform/tests/custom-elements/pseudo-class-defined-customized-builtins.html81
-rw-r--r--testing/web-platform/tests/custom-elements/pseudo-class-defined-print-ref.html9
-rw-r--r--testing/web-platform/tests/custom-elements/pseudo-class-defined-print.html17
-rw-r--r--testing/web-platform/tests/custom-elements/pseudo-class-defined.html103
-rw-r--r--testing/web-platform/tests/custom-elements/range-and-constructors.html61
-rw-r--r--testing/web-platform/tests/custom-elements/reaction-timing.html113
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/Animation.html66
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/AriaMixin-element-attributes.html65
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/AriaMixin-string-attributes.html66
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/Attr.html23
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/CSSStyleDeclaration.html89
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/ChildNode.html35
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/DOMStringMap.html96
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/DOMTokenList.html210
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/Document.html156
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/Element.html91
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/ElementContentEditable.html21
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/HTMLAnchorElement.html32
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/HTMLElement.html38
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/HTMLOptionElement.html35
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/HTMLOptionsCollection.html122
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/HTMLOutputElement.html45
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/HTMLSelectElement.html122
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/HTMLTableElement.html173
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/HTMLTableRowElement.html34
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/HTMLTableSectionElement.html45
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/HTMLTitleElement.html35
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/NamedNodeMap.html39
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/Node.html49
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/ParentNode.html27
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/Range.html54
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/Selection.html32
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/ShadowRoot.html52
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLAreaElement.html69
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLBaseElement.html21
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLButtonElement.html80
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLCanvasElement.html16
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLDataElement.html15
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLDetailsElement.html15
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLEmbedElement.html35
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLFieldSetElement.html32
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLImageElement.html89
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLInputElement.html66
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLLIElement.html38
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLLabelElement.html29
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLMapElement.html17
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLMediaElement.html107
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLMetaElement.html77
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLMeterElement.html55
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLModElement.html18
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLOListElement.html17
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLOptGroupElement.html38
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLParamElement.html41
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLProgressElement.html25
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLQuoteElement.html16
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLSlotElement.html15
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLSourceElement.html191
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLStyleElement.html23
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLTableCellElement.html63
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLTableColElement.html25
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLTimeElement.html15
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/resources/reactions.js452
-rw-r--r--testing/web-platform/tests/custom-elements/reactions/with-exceptions.html35
-rw-r--r--testing/web-platform/tests/custom-elements/resources/custom-elements-helpers.js276
-rw-r--r--testing/web-platform/tests/custom-elements/resources/empty-html-document.html5
-rw-r--r--testing/web-platform/tests/custom-elements/resources/my-custom-element-html-document.html6
-rw-r--r--testing/web-platform/tests/custom-elements/resources/navigation-destination.html9
-rw-r--r--testing/web-platform/tests/custom-elements/scoped-registry/CustomElementRegistry-constructor.tentative.html27
-rw-r--r--testing/web-platform/tests/custom-elements/scoped-registry/CustomElementRegistry-multi-register.tentative.html27
-rw-r--r--testing/web-platform/tests/custom-elements/scoped-registry/ShadowRoot-init-registry.tentative.html52
-rw-r--r--testing/web-platform/tests/custom-elements/scoped-registry/ShadowRoot-innerHTML-upgrade.tentative.html116
-rw-r--r--testing/web-platform/tests/custom-elements/scoped-registry/constructor-call.tentative.html42
-rw-r--r--testing/web-platform/tests/custom-elements/scoped-registry/constructor-reentry-with-different-definition.tentative.html137
-rw-r--r--testing/web-platform/tests/custom-elements/state/tentative/ElementInternals-states.html82
-rw-r--r--testing/web-platform/tests/custom-elements/state/tentative/README.md1
-rw-r--r--testing/web-platform/tests/custom-elements/state/tentative/state-pseudo-class.html132
-rw-r--r--testing/web-platform/tests/custom-elements/throw-on-dynamic-markup-insertion-counter-construct-xml-parser.xhtml134
-rw-r--r--testing/web-platform/tests/custom-elements/throw-on-dynamic-markup-insertion-counter-construct.html117
-rw-r--r--testing/web-platform/tests/custom-elements/throw-on-dynamic-markup-insertion-counter-reactions-xml-parser.xhtml134
-rw-r--r--testing/web-platform/tests/custom-elements/throw-on-dynamic-markup-insertion-counter-reactions.html117
-rw-r--r--testing/web-platform/tests/custom-elements/upgrading.html258
-rw-r--r--testing/web-platform/tests/custom-elements/upgrading/Document-importNode-customized-builtins.html32
-rw-r--r--testing/web-platform/tests/custom-elements/upgrading/Document-importNode.html32
-rw-r--r--testing/web-platform/tests/custom-elements/upgrading/Node-cloneNode-customized-builtins.html48
-rw-r--r--testing/web-platform/tests/custom-elements/upgrading/Node-cloneNode.html206
-rw-r--r--testing/web-platform/tests/custom-elements/upgrading/upgrading-enqueue-reactions.html158
-rw-r--r--testing/web-platform/tests/custom-elements/upgrading/upgrading-parser-created-element.html125
-rw-r--r--testing/web-platform/tests/custom-elements/when-defined-reentry-crash.html25
-rw-r--r--testing/web-platform/tests/custom-elements/xhtml-crash.xhtml16
151 files changed, 13427 insertions, 0 deletions
diff --git a/testing/web-platform/tests/custom-elements/CustomElementRegistry-constructor-and-callbacks-are-held-strongly.html b/testing/web-platform/tests/custom-elements/CustomElementRegistry-constructor-and-callbacks-are-held-strongly.html
new file mode 100644
index 0000000000..fb6af32fe1
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/CustomElementRegistry-constructor-and-callbacks-are-held-strongly.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CustomElementInterface holds constructors and callbacks strongly, preventing them from being GCed if there are no other references</title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/custom-elements.html#concept-custom-element-definition-lifecycle-callbacks">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/gc.js"></script>
+
+<body>
+<div id="customElementsRoot"></div>
+<iframe id="emptyIframe" srcdoc></iframe>
+<script>
+"use strict";
+
+const tagNames = [...new Array(100)].map((_, i) => `x-foo${i}`);
+const delay = (t, ms) => new Promise(resolve => { t.step_timeout(resolve, ms); });
+
+const connectedCallbackCalls = new Set;
+const disconnectedCallbackCalls = new Set;
+const attributeChangedCallbackCalls = new Set;
+const adoptedCallbackCalls = new Set;
+
+for (const tagName of tagNames) {
+ const constructor = class extends HTMLElement {
+ connectedCallback() { connectedCallbackCalls.add(tagName); }
+ disconnectedCallback() { disconnectedCallbackCalls.add(tagName); }
+ attributeChangedCallback() { attributeChangedCallbackCalls.add(tagName); }
+ adoptedCallback() { adoptedCallbackCalls.add(tagName); }
+ };
+
+ constructor.observedAttributes = ["foo"];
+
+ customElements.define(tagName, constructor);
+
+ delete constructor.prototype.connectedCallback;
+ delete constructor.prototype.disconnectedCallback;
+ delete constructor.prototype.attributeChangedCallback;
+ delete constructor.prototype.adoptedCallback;
+}
+
+promise_test(async t => {
+ await garbageCollect();
+
+ assert_true(tagNames.every(tagName => typeof customElements.get(tagName) === "function"));
+}, "constructor");
+
+promise_test(async t => {
+ await garbageCollect();
+ for (const tagName of tagNames) {
+ customElementsRoot.append(document.createElement(tagName));
+ }
+
+ await delay(t, 10);
+ assert_equals(connectedCallbackCalls.size, tagNames.length);
+}, "connectedCallback");
+
+promise_test(async t => {
+ await garbageCollect();
+ for (const xFoo of customElementsRoot.children) {
+ xFoo.setAttribute("foo", "bar");
+ }
+
+ await delay(t, 10);
+ assert_equals(attributeChangedCallbackCalls.size, tagNames.length);
+}, "attributeChangedCallback");
+
+promise_test(async t => {
+ await garbageCollect();
+ customElementsRoot.innerHTML = "";
+
+ await delay(t, 10);
+ assert_equals(disconnectedCallbackCalls.size, tagNames.length);
+}, "disconnectedCallback");
+
+promise_test(async t => {
+ await garbageCollect();
+ for (const tagName of tagNames) {
+ emptyIframe.contentDocument.adoptNode(document.createElement(tagName));
+ }
+
+ await delay(t, 10);
+ assert_equals(adoptedCallbackCalls.size, tagNames.length);
+}, "adoptedCallback");
+</script>
diff --git a/testing/web-platform/tests/custom-elements/CustomElementRegistry.html b/testing/web-platform/tests/custom-elements/CustomElementRegistry.html
new file mode 100644
index 0000000000..5b75fc651f
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/CustomElementRegistry.html
@@ -0,0 +1,755 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: CustomElementRegistry interface</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="CustomElementRegistry interface must exist">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+test(function () {
+ assert_true('define' in CustomElementRegistry.prototype, '"define" exists on CustomElementRegistry.prototype');
+ assert_true('define' in customElements, '"define" exists on window.customElements');
+}, 'CustomElementRegistry interface must have define as a method');
+
+test(function () {
+ assert_throws_js(TypeError, function () { customElements.define('badname', 1); },
+ 'customElements.define must throw a TypeError when the element interface is a number');
+ assert_throws_js(TypeError, function () { customElements.define('badname', '123'); },
+ 'customElements.define must throw a TypeError when the element interface is a string');
+ assert_throws_js(TypeError, function () { customElements.define('badname', {}); },
+ 'customElements.define must throw a TypeError when the element interface is an object');
+ assert_throws_js(TypeError, function () { customElements.define('badname', []); },
+ 'customElements.define must throw a TypeError when the element interface is an array');
+}, 'customElements.define must throw when the element interface is not a constructor');
+
+test(function () {
+ customElements.define('custom-html-element', HTMLElement);
+}, 'customElements.define must not throw the constructor is HTMLElement');
+
+test(function () {
+ class MyCustomElement extends HTMLElement {};
+
+ assert_throws_dom('SyntaxError', function () { customElements.define(null, MyCustomElement); },
+ 'customElements.define must throw a SyntaxError if the tag name is null');
+ assert_throws_dom('SyntaxError', function () { customElements.define('', MyCustomElement); },
+ 'customElements.define must throw a SyntaxError if the tag name is empty');
+ assert_throws_dom('SyntaxError', function () { customElements.define('abc', MyCustomElement); },
+ 'customElements.define must throw a SyntaxError if the tag name does not contain "-"');
+ assert_throws_dom('SyntaxError', function () { customElements.define('a-Bc', MyCustomElement); },
+ 'customElements.define must throw a SyntaxError if the tag name contains an upper case letter');
+
+ var builtinTagNames = [
+ 'annotation-xml',
+ 'color-profile',
+ 'font-face',
+ 'font-face-src',
+ 'font-face-uri',
+ 'font-face-format',
+ 'font-face-name',
+ 'missing-glyph'
+ ];
+
+ for (var tagName of builtinTagNames) {
+ assert_throws_dom('SyntaxError', function () { customElements.define(tagName, MyCustomElement); },
+ 'customElements.define must throw a SyntaxError if the tag name is "' + tagName + '"');
+ }
+
+}, 'customElements.define must throw with an invalid name');
+
+test(function () {
+ class SomeCustomElement extends HTMLElement {};
+
+ var calls = [];
+ var OtherCustomElement = new Proxy(class extends HTMLElement {}, {
+ get: function (target, name) {
+ calls.push(name);
+ return target[name];
+ }
+ })
+
+ customElements.define('some-custom-element', SomeCustomElement);
+ assert_throws_dom('NotSupportedError', function () { customElements.define('some-custom-element', OtherCustomElement); },
+ 'customElements.define must throw a NotSupportedError if the specified tag name is already used');
+ assert_array_equals(calls, [], 'customElements.define must validate the custom element name before getting the prototype of the constructor');
+
+}, 'customElements.define must throw when there is already a custom element of the same name');
+
+test(function () {
+ class AnotherCustomElement extends HTMLElement {};
+
+ customElements.define('another-custom-element', AnotherCustomElement);
+ assert_throws_dom('NotSupportedError', function () { customElements.define('some-other-element', AnotherCustomElement); },
+ 'customElements.define must throw a NotSupportedError if the specified class already defines an element');
+
+}, 'customElements.define must throw a NotSupportedError when there is already a custom element with the same class');
+
+test(function () {
+ var outerCalls = [];
+ var OuterCustomElement = new Proxy(class extends HTMLElement { }, {
+ get: function (target, name) {
+ outerCalls.push(name);
+ customElements.define('inner-custom-element', InnerCustomElement);
+ return target[name];
+ }
+ });
+ var innerCalls = [];
+ var InnerCustomElement = new Proxy(class extends HTMLElement { }, {
+ get: function (target, name) {
+ outerCalls.push(name);
+ return target[name];
+ }
+ });
+
+ assert_throws_dom('NotSupportedError', function () { customElements.define('outer-custom-element', OuterCustomElement); },
+ 'customElements.define must throw a NotSupportedError if the specified class already defines an element');
+ assert_array_equals(outerCalls, ['prototype'], 'customElements.define must get "prototype"');
+ assert_array_equals(innerCalls, [],
+ 'customElements.define must throw a NotSupportedError when element definition is running flag is set'
+ + ' before getting the prototype of the constructor');
+
+}, 'customElements.define must throw a NotSupportedError when element definition is running flag is set');
+
+test(function () {
+ var calls = [];
+ var ElementWithBadInnerConstructor = new Proxy(class extends HTMLElement { }, {
+ get: function (target, name) {
+ calls.push(name);
+ customElements.define('inner-custom-element', 1);
+ return target[name];
+ }
+ });
+
+ assert_throws_js(TypeError, function () {
+ customElements.define('element-with-bad-inner-constructor', ElementWithBadInnerConstructor);
+ }, 'customElements.define must throw a NotSupportedError if IsConstructor(constructor) is false');
+
+ assert_array_equals(calls, ['prototype'], 'customElements.define must get "prototype"');
+}, 'customElements.define must check IsConstructor on the constructor before checking the element definition is running flag');
+
+test(function () {
+ var calls = [];
+ var ElementWithBadInnerName = new Proxy(class extends HTMLElement { }, {
+ get: function (target, name) {
+ calls.push(name);
+ customElements.define('badname', class extends HTMLElement {});
+ return target[name];
+ }
+ });
+
+ assert_throws_dom('SyntaxError', function () {
+ customElements.define('element-with-bad-inner-name', ElementWithBadInnerName);
+ }, 'customElements.define must throw a SyntaxError if the specified name is not a valid custom element name');
+
+ assert_array_equals(calls, ['prototype'], 'customElements.define must get "prototype"');
+}, 'customElements.define must validate the custom element name before checking the element definition is running flag');
+
+test(function () {
+ var unresolvedElement = document.createElement('constructor-calls-define');
+ document.body.appendChild(unresolvedElement);
+ var elementUpgradedDuringUpgrade = document.createElement('defined-during-upgrade');
+ document.body.appendChild(elementUpgradedDuringUpgrade);
+
+ var DefinedDuringUpgrade = class extends HTMLElement { };
+
+ class ConstructorCallsDefine extends HTMLElement {
+ constructor() {
+ customElements.define('defined-during-upgrade', DefinedDuringUpgrade);
+ assert_false(unresolvedElement instanceof ConstructorCallsDefine);
+ assert_true(elementUpgradedDuringUpgrade instanceof DefinedDuringUpgrade);
+ super();
+ assert_true(unresolvedElement instanceof ConstructorCallsDefine);
+ assert_true(elementUpgradedDuringUpgrade instanceof DefinedDuringUpgrade);
+ }
+ }
+
+ assert_false(unresolvedElement instanceof ConstructorCallsDefine);
+ assert_false(elementUpgradedDuringUpgrade instanceof DefinedDuringUpgrade);
+
+ customElements.define('constructor-calls-define', ConstructorCallsDefine);
+}, 'customElements.define unset the element definition is running flag before upgrading custom elements');
+
+(function () {
+ var testCase = async_test('customElements.define must not throw'
+ +' when defining another custom element in a different global object during Get(constructor, "prototype")');
+
+ var iframe = document.createElement('iframe');
+ iframe.onload = function () {
+ testCase.step(function () {
+ var InnerCustomElement = class extends iframe.contentWindow.HTMLElement {};
+ var calls = [];
+ var proxy = new Proxy(class extends HTMLElement { }, {
+ get: function (target, name) {
+ calls.push(name);
+ if (name === "prototype") {
+ iframe.contentWindow.customElements.define('another-custom-element', InnerCustomElement);
+ }
+ return target[name];
+ }
+ })
+ customElements.define('element-with-inner-element-define', proxy);
+ assert_array_equals(calls, ['prototype', 'disabledFeatures', 'formAssociated'],
+ 'customElements.define must get "prototype", "disabledFeatures", and "formAssociated" on the constructor');
+ assert_true(iframe.contentDocument.createElement('another-custom-element') instanceof InnerCustomElement);
+ });
+ document.body.removeChild(iframe);
+ testCase.done();
+ }
+
+ document.body.appendChild(iframe);
+})();
+
+test(function () {
+ var calls = [];
+ var ElementWithBadInnerName = new Proxy(class extends HTMLElement { }, {
+ get: function (target, name) {
+ calls.push(name);
+ customElements.define('badname', class extends HTMLElement {});
+ return target[name];
+ }
+ });
+
+ assert_throws_dom('SyntaxError', function () {
+ customElements.define('element-with-bad-inner-name', ElementWithBadInnerName);
+ }, 'customElements.define must throw a SyntaxError if the specified name is not a valid custom element name');
+
+ assert_array_equals(calls, ['prototype'], 'customElements.define must get "prototype"');
+}, '');
+
+test(function () {
+ var calls = [];
+ var proxy = new Proxy(class extends HTMLElement { }, {
+ get: function (target, name) {
+ calls.push(name);
+ return target[name];
+ }
+ });
+ customElements.define('proxy-element', proxy);
+ assert_array_equals(calls, ['prototype', 'disabledFeatures', 'formAssociated']);
+}, 'customElements.define must get "prototype", "disabledFeatures", and "formAssociated" property of the constructor');
+
+test(function () {
+ var err = {name: 'expectedError'}
+ var proxy = new Proxy(class extends HTMLElement { }, {
+ get: function (target, name) {
+ throw err;
+ }
+ });
+ assert_throws_exactly(err, function () { customElements.define('element-with-string-prototype', proxy); });
+}, 'customElements.define must rethrow an exception thrown while getting "prototype" property of the constructor');
+
+test(function () {
+ var returnedValue;
+ var proxy = new Proxy(class extends HTMLElement { }, {
+ get: function (target, name) { return returnedValue; }
+ });
+
+ returnedValue = null;
+ assert_throws_js(TypeError, function () { customElements.define('element-with-string-prototype', proxy); },
+ 'customElements.define must throw when "prototype" property of the constructor is null');
+ returnedValue = undefined;
+ assert_throws_js(TypeError, function () { customElements.define('element-with-string-prototype', proxy); },
+ 'customElements.define must throw when "prototype" property of the constructor is undefined');
+ returnedValue = 'hello';
+ assert_throws_js(TypeError, function () { customElements.define('element-with-string-prototype', proxy); },
+ 'customElements.define must throw when "prototype" property of the constructor is a string');
+ returnedValue = 1;
+ assert_throws_js(TypeError, function () { customElements.define('element-with-string-prototype', proxy); },
+ 'customElements.define must throw when "prototype" property of the constructor is a number');
+
+}, 'customElements.define must throw when "prototype" property of the constructor is not an object');
+
+test(function () {
+ var constructor = function () {}
+ var calls = [];
+ constructor.prototype = new Proxy(constructor.prototype, {
+ get: function (target, name) {
+ calls.push(name);
+ return target[name];
+ }
+ });
+ customElements.define('element-with-proxy-prototype', constructor);
+ assert_array_equals(calls, ['connectedCallback', 'disconnectedCallback', 'adoptedCallback', 'attributeChangedCallback']);
+}, 'customElements.define must get callbacks of the constructor prototype');
+
+test(function () {
+ var constructor = function () {}
+ var calls = [];
+ var err = {name: 'expectedError'}
+ constructor.prototype = new Proxy(constructor.prototype, {
+ get: function (target, name) {
+ calls.push(name);
+ if (name == 'disconnectedCallback')
+ throw err;
+ return target[name];
+ }
+ });
+ assert_throws_exactly(err, function () { customElements.define('element-with-throwing-callback', constructor); });
+ assert_array_equals(calls, ['connectedCallback', 'disconnectedCallback'],
+ 'customElements.define must not get callbacks after one of the get throws');
+}, 'customElements.define must rethrow an exception thrown while getting callbacks on the constructor prototype');
+
+test(function () {
+ var constructor = function () {}
+ var calls = [];
+ constructor.prototype = new Proxy(constructor.prototype, {
+ get: function (target, name) {
+ calls.push(name);
+ if (name == 'adoptedCallback')
+ return 1;
+ return target[name];
+ }
+ });
+ assert_throws_js(TypeError, function () { customElements.define('element-with-throwing-callback', constructor); });
+ assert_array_equals(calls, ['connectedCallback', 'disconnectedCallback', 'adoptedCallback'],
+ 'customElements.define must not get callbacks after one of the conversion throws');
+}, 'customElements.define must rethrow an exception thrown while converting a callback value to Function callback type');
+
+test(function () {
+ var constructor = function () {}
+ constructor.prototype.attributeChangedCallback = function () { };
+ var prototypeCalls = [];
+ var callOrder = 0;
+ constructor.prototype = new Proxy(constructor.prototype, {
+ get: function (target, name) {
+ if (name == 'prototype' || name == 'observedAttributes')
+ throw 'Unexpected access to observedAttributes';
+ prototypeCalls.push(callOrder++);
+ prototypeCalls.push(name);
+ return target[name];
+ }
+ });
+ var constructorCalls = [];
+ var proxy = new Proxy(constructor, {
+ get: function (target, name) {
+ constructorCalls.push(callOrder++);
+ constructorCalls.push(name);
+ return target[name];
+ }
+ });
+ customElements.define('element-with-attribute-changed-callback', proxy);
+ assert_array_equals(prototypeCalls, [1, 'connectedCallback', 2, 'disconnectedCallback', 3, 'adoptedCallback', 4, 'attributeChangedCallback']);
+ assert_array_equals(constructorCalls, [0, 'prototype',
+ 5, 'observedAttributes',
+ 6, 'disabledFeatures',
+ 7, 'formAssociated']);
+}, 'customElements.define must get "observedAttributes" property on the constructor prototype when "attributeChangedCallback" is present');
+
+test(function () {
+ var constructor = function () {}
+ constructor.prototype.attributeChangedCallback = function () { };
+ var calls = [];
+ var err = {name: 'expectedError'};
+ var proxy = new Proxy(constructor, {
+ get: function (target, name) {
+ calls.push(name);
+ if (name == 'observedAttributes')
+ throw err;
+ return target[name];
+ }
+ });
+ assert_throws_exactly(err, function () { customElements.define('element-with-throwing-observed-attributes', proxy); });
+ assert_array_equals(calls, ['prototype', 'observedAttributes'],
+ 'customElements.define must get "prototype" and "observedAttributes" on the constructor');
+}, 'customElements.define must rethrow an exception thrown while getting observedAttributes on the constructor prototype');
+
+test(function () {
+ var constructor = function () {}
+ constructor.prototype.attributeChangedCallback = function () { };
+ var calls = [];
+ var proxy = new Proxy(constructor, {
+ get: function (target, name) {
+ calls.push(name);
+ if (name == 'observedAttributes')
+ return 1;
+ return target[name];
+ }
+ });
+ assert_throws_js(TypeError, function () { customElements.define('element-with-invalid-observed-attributes', proxy); });
+ assert_array_equals(calls, ['prototype', 'observedAttributes'],
+ 'customElements.define must get "prototype" and "observedAttributes" on the constructor');
+}, 'customElements.define must rethrow an exception thrown while converting the value of observedAttributes to sequence<DOMString>');
+
+ test(function () {
+ var err = {name: 'SomeError'};
+ var constructor = function () {}
+ constructor.prototype.attributeChangedCallback = function () { };
+ constructor.observedAttributes = {[Symbol.iterator]: function *() {
+ yield 'foo';
+ throw err;
+ }};
+ assert_throws_exactly(err, function () { customElements.define('element-with-generator-observed-attributes', constructor); });
+}, 'customElements.define must rethrow an exception thrown while iterating over observedAttributes to sequence<DOMString>');
+
+test(function () {
+ var constructor = function () {}
+ constructor.prototype.attributeChangedCallback = function () { };
+ constructor.observedAttributes = {[Symbol.iterator]: 1};
+ assert_throws_js(TypeError, function () { customElements.define('element-with-observed-attributes-with-uncallable-iterator', constructor); });
+}, 'customElements.define must rethrow an exception thrown while retrieving Symbol.iterator on observedAttributes');
+
+test(function () {
+ var constructor = function () {}
+ constructor.observedAttributes = 1;
+ customElements.define('element-without-callback-with-invalid-observed-attributes', constructor);
+}, 'customElements.define must not throw even if "observedAttributes" fails to convert if "attributeChangedCallback" is not defined');
+
+test(function () {
+ var constructor = function () {}
+ var calls = [];
+ var err = {name: 'expectedError'}
+ var proxy = new Proxy(constructor, {
+ get: function (target, name) {
+ calls.push(name);
+ if (name == 'disabledFeatures')
+ throw err;
+ return target[name];
+ }
+ });
+ assert_throws_exactly(err, () => customElements.define('element-with-throwing-disabled-features', proxy));
+ assert_array_equals(calls, ['prototype', 'disabledFeatures'],
+ 'customElements.define must get "prototype" and "disabledFeatures" on the constructor');
+}, 'customElements.define must rethrow an exception thrown while getting disabledFeatures on the constructor prototype');
+
+test(function () {
+ var constructor = function () {}
+ var calls = [];
+ var proxy = new Proxy(constructor, {
+ get: function (target, name) {
+ calls.push(name);
+ if (name == 'disabledFeatures')
+ return 1;
+ return target[name];
+ }
+ });
+ assert_throws_js(TypeError, () => customElements.define('element-with-invalid-disabled-features', proxy));
+ assert_array_equals(calls, ['prototype', 'disabledFeatures'],
+ 'customElements.define must get "prototype" and "disabledFeatures" on the constructor');
+}, 'customElements.define must rethrow an exception thrown while converting the value of disabledFeatures to sequence<DOMString>');
+
+test(function () {
+ var constructor = function () {}
+ var err = {name: 'SomeError'};
+ constructor.disabledFeatures = {[Symbol.iterator]: function *() {
+ yield 'foo';
+ throw err;
+ }};
+ assert_throws_exactly(err, () => customElements.define('element-with-generator-disabled-features', constructor));
+}, 'customElements.define must rethrow an exception thrown while iterating over disabledFeatures to sequence<DOMString>');
+
+test(function () {
+ var constructor = function () {}
+ constructor.disabledFeatures = {[Symbol.iterator]: 1};
+ assert_throws_js(TypeError, () => customElements.define('element-with-disabled-features-with-uncallable-iterator', constructor));
+}, 'customElements.define must rethrow an exception thrown while retrieving Symbol.iterator on disabledFeatures');
+
+test(function () {
+ var constructor = function () {}
+ var calls = [];
+ var err = {name: 'expectedError'};
+ var proxy = new Proxy(constructor, {
+ get: function (target, name) {
+ calls.push(name);
+ if (name == 'formAssociated')
+ throw err;
+ return target[name];
+ }
+ });
+ assert_throws_exactly(err,
+ () => customElements.define('element-with-throwing-form-associated', proxy));
+ assert_array_equals(calls, ['prototype', 'disabledFeatures', 'formAssociated'],
+ 'customElements.define must get "prototype", "disabledFeatures", and ' +
+ '"formAssociated" on the constructor');
+}, 'customElements.define must rethrow an exception thrown while getting ' +
+ 'formAssociated on the constructor prototype');
+
+test(function () {
+ var constructor = function () {}
+ var prototypeCalls = [];
+ constructor.prototype = new Proxy(constructor.prototype, {
+ get: function(target, name) {
+ prototypeCalls.push(name)
+ return target[name];
+ }
+ });
+ var constructorCalls = [];
+ var proxy = new Proxy(constructor, {
+ get: function (target, name) {
+ constructorCalls.push(name);
+ if (name == 'formAssociated')
+ return 1;
+ return target[name];
+ }
+ });
+ customElements.define('element-with-form-associated-true', proxy);
+ assert_array_equals(constructorCalls,
+ ['prototype', 'disabledFeatures', 'formAssociated'],
+ 'customElements.define must get "prototype", "disabledFeatures", and ' +
+ '"formAssociated" on the constructor');
+ assert_array_equals(
+ prototypeCalls,
+ ['connectedCallback', 'disconnectedCallback', 'adoptedCallback',
+ 'attributeChangedCallback', 'formAssociatedCallback',
+ 'formResetCallback', 'formDisabledCallback',
+ 'formStateRestoreCallback'],
+ 'customElements.define must get 8 callbacks on the prototype');
+}, 'customElements.define must get four additional callbacks on the prototype' +
+ ' if formAssociated is converted to true');
+
+test(function () {
+ var constructor = function() {};
+ var proxy = new Proxy(constructor, {
+ get: function(target, name) {
+ if (name == 'formAssociated')
+ return {}; // Any object is converted to 'true'.
+ return target[name];
+ }
+ });
+ var calls = [];
+ var err = {name: 'expectedError'};
+ constructor.prototype = new Proxy(constructor.prototype, {
+ get: function (target, name) {
+ calls.push(name);
+ if (name == 'formDisabledCallback')
+ throw err;
+ return target[name];
+ }
+ });
+ assert_throws_exactly(err,
+ () => customElements.define('element-with-throwing-callback-2', proxy));
+ assert_array_equals(calls, ['connectedCallback', 'disconnectedCallback',
+ 'adoptedCallback', 'attributeChangedCallback',
+ 'formAssociatedCallback', 'formResetCallback',
+ 'formDisabledCallback'],
+ 'customElements.define must not get callbacks after one of the get throws');
+
+ var calls2 = [];
+ constructor.prototype = new Proxy(constructor.prototype, {
+ get: function (target, name) {
+ calls2.push(name);
+ if (name == 'formResetCallback')
+ return 43; // Can't convert to a Function.
+ return target[name];
+ }
+ });
+ assert_throws_js(TypeError,
+ () => customElements.define('element-with-throwing-callback-3', proxy));
+ assert_array_equals(calls2, ['connectedCallback', 'disconnectedCallback',
+ 'adoptedCallback', 'attributeChangedCallback',
+ 'formAssociatedCallback', 'formResetCallback'],
+ 'customElements.define must not get callbacks after one of the get throws');
+}, 'customElements.define must rethrow an exception thrown while getting ' +
+ 'additional formAssociated callbacks on the constructor prototype');
+
+test(function () {
+ class MyCustomElement extends HTMLElement {};
+ customElements.define('my-custom-element', MyCustomElement);
+
+ var instance = new MyCustomElement;
+ assert_true(instance instanceof MyCustomElement,
+ 'An instance of a custom HTML element be an instance of the associated interface');
+
+ assert_true(instance instanceof HTMLElement,
+ 'An instance of a custom HTML element must inherit from HTMLElement');
+
+ assert_equals(instance.localName, 'my-custom-element',
+ 'An instance of a custom element must use the associated tag name');
+
+ assert_equals(instance.namespaceURI, 'http://www.w3.org/1999/xhtml',
+ 'A custom element HTML must use HTML namespace');
+
+}, 'customElements.define must define an instantiatable custom element');
+
+test(function () {
+ var disconnectedElement = document.createElement('some-custom');
+ var connectedElementBeforeShadowHost = document.createElement('some-custom');
+ var connectedElementAfterShadowHost = document.createElement('some-custom');
+ var elementInShadowTree = document.createElement('some-custom');
+ var childElementOfShadowHost = document.createElement('some-custom');
+ var customShadowHost = document.createElement('some-custom');
+ var elementInNestedShadowTree = document.createElement('some-custom');
+
+ var container = document.createElement('div');
+ var shadowHost = document.createElement('div');
+ var shadowRoot = shadowHost.attachShadow({mode: 'closed'});
+ container.appendChild(connectedElementBeforeShadowHost);
+ container.appendChild(shadowHost);
+ container.appendChild(connectedElementAfterShadowHost);
+ shadowHost.appendChild(childElementOfShadowHost);
+ shadowRoot.appendChild(elementInShadowTree);
+ shadowRoot.appendChild(customShadowHost);
+
+ var innerShadowRoot = customShadowHost.attachShadow({mode: 'closed'});
+ innerShadowRoot.appendChild(elementInNestedShadowTree);
+
+ var calls = [];
+ class SomeCustomElement extends HTMLElement {
+ constructor() {
+ super();
+ calls.push(this);
+ }
+ };
+
+ document.body.appendChild(container);
+ customElements.define('some-custom', SomeCustomElement);
+ assert_array_equals(calls, [connectedElementBeforeShadowHost, elementInShadowTree, customShadowHost, elementInNestedShadowTree, childElementOfShadowHost, connectedElementAfterShadowHost]);
+}, 'customElements.define must upgrade elements in the shadow-including tree order');
+
+test(function () {
+ assert_true('get' in CustomElementRegistry.prototype, '"get" exists on CustomElementRegistry.prototype');
+ assert_true('get' in customElements, '"get" exists on window.customElements');
+}, 'CustomElementRegistry interface must have get as a method');
+
+test(function () {
+ assert_equals(customElements.get('a-b'), undefined);
+}, 'customElements.get must return undefined when the registry does not contain an entry with the given name');
+
+test(function () {
+ assert_equals(customElements.get('html'), undefined);
+ assert_equals(customElements.get('span'), undefined);
+ assert_equals(customElements.get('div'), undefined);
+ assert_equals(customElements.get('g'), undefined);
+ assert_equals(customElements.get('ab'), undefined);
+}, 'customElements.get must return undefined when the registry does not contain an entry with the given name even if the name was not a valid custom element name');
+
+test(function () {
+ assert_equals(customElements.get('existing-custom-element'), undefined);
+ class ExistingCustomElement extends HTMLElement {};
+ customElements.define('existing-custom-element', ExistingCustomElement);
+ assert_equals(customElements.get('existing-custom-element'), ExistingCustomElement);
+}, 'customElements.get return the constructor of the entry with the given name when there is a matching entry.');
+
+test(function () {
+ assert_true(customElements.whenDefined('some-name') instanceof Promise);
+}, 'customElements.whenDefined must return a promise for a valid custom element name');
+
+test(function () {
+ assert_equals(customElements.whenDefined('some-name'), customElements.whenDefined('some-name'));
+}, 'customElements.whenDefined must return the same promise each time invoked for a valid custom element name which has not been defined');
+
+promise_test(function () {
+ var resolved = false;
+ var rejected = false;
+ customElements.whenDefined('a-b').then(function () { resolved = true; }, function () { rejected = true; });
+ return Promise.resolve().then(function () {
+ assert_false(resolved, 'The promise returned by "whenDefined" must not be resolved until a custom element is defined');
+ assert_false(rejected, 'The promise returned by "whenDefined" must not be rejected until a custom element is defined');
+ });
+}, 'customElements.whenDefined must return an unresolved promise when the registry does not contain the entry with the given name')
+
+promise_test(function () {
+ var promise = customElements.whenDefined('badname');
+ promise.then(function (value) { promise.resolved = value; }, function (value) { promise.rejected = value; });
+
+ assert_false('resolved' in promise, 'The promise returned by "whenDefined" must not be resolved until the end of the next microtask');
+ assert_false('rejected' in promise, 'The promise returned by "whenDefined" must not be rejected until the end of the next microtask');
+
+ return Promise.resolve().then(function () {
+ assert_false('resolved' in promise, 'The promise returned by "whenDefined" must be resolved when a custom element is defined');
+ assert_true('rejected' in promise, 'The promise returned by "whenDefined" must not be rejected when a custom element is defined');
+ });
+}, 'customElements.whenDefined must return a rejected promise when the given name is not a valid custom element name');
+
+promise_test(function () {
+ class PreexistingCustomElement extends HTMLElement { };
+ customElements.define('preexisting-custom-element', PreexistingCustomElement);
+
+ var promise = customElements.whenDefined('preexisting-custom-element');
+ promise.then(function (value) { promise.resolved = value; }, function (value) { promise.rejected = value; });
+
+ assert_false('resolved' in promise, 'The promise returned by "whenDefined" must not be resolved until the end of the next microtask');
+ assert_false('rejected' in promise, 'The promise returned by "whenDefined" must not be rejected until the end of the next microtask');
+
+ return Promise.resolve().then(function () {
+ assert_true('resolved' in promise, 'The promise returned by "whenDefined" must be resolved when a custom element is defined');
+ assert_equals(promise.resolved, PreexistingCustomElement,
+ 'The promise returned by "whenDefined" must be resolved with the constructor of the element when a custom element is defined');
+ assert_false('rejected' in promise, 'The promise returned by "whenDefined" must not be rejected when a custom element is defined');
+ });
+}, 'customElements.whenDefined must return a resolved promise when the registry contains the entry with the given name');
+
+promise_test(function () {
+ class AnotherExistingCustomElement extends HTMLElement {};
+ customElements.define('another-existing-custom-element', AnotherExistingCustomElement);
+
+ var promise1 = customElements.whenDefined('another-existing-custom-element');
+ var promise2 = customElements.whenDefined('another-existing-custom-element');
+ promise1.then(function (value) { promise1.resolved = value; }, function (value) { promise1.rejected = value; });
+ promise2.then(function (value) { promise2.resolved = value; }, function (value) { promise2.rejected = value; });
+
+ assert_not_equals(promise1, promise2);
+ assert_false('resolved' in promise1, 'The promise returned by "whenDefined" must not be resolved until the end of the next microtask');
+ assert_false('resolved' in promise2, 'The promise returned by "whenDefined" must not be resolved until the end of the next microtask');
+ assert_false('rejected' in promise1, 'The promise returned by "whenDefined" must not be rejected until the end of the next microtask');
+ assert_false('rejected' in promise2, 'The promise returned by "whenDefined" must not be rejected until the end of the next microtask');
+
+ return Promise.resolve().then(function () {
+ assert_true('resolved' in promise1, 'The promise returned by "whenDefined" must be resolved when a custom element is defined');
+ assert_equals(promise1.resolved, AnotherExistingCustomElement, 'The promise returned by "whenDefined" must be resolved with the constructor of the element when a custom element is defined');
+ assert_false('rejected' in promise1, 'The promise returned by "whenDefined" must not be rejected when a custom element is defined');
+
+ assert_true('resolved' in promise2, 'The promise returned by "whenDefined" must be resolved when a custom element is defined');
+ assert_equals(promise2.resolved, AnotherExistingCustomElement, 'The promise returned by "whenDefined" must be resolved with the constructor of the element when a custom element is defined');
+ assert_false('rejected' in promise2, 'The promise returned by "whenDefined" must not be rejected when a custom element is defined');
+ });
+}, 'customElements.whenDefined must return a new resolved promise each time invoked when the registry contains the entry with the given name');
+
+promise_test(function () {
+ class ElementDefinedAfterWhenDefined extends HTMLElement { };
+ var promise = customElements.whenDefined('element-defined-after-whendefined');
+ promise.then(function (value) { promise.resolved = value; }, function (value) { promise.rejected = value; });
+
+ assert_false('resolved' in promise, 'The promise returned by "whenDefined" must not be resolved until the end of the next microtask');
+ assert_false('rejected' in promise, 'The promise returned by "whenDefined" must not be rejected until the end of the next microtask');
+
+ var promiseAfterDefine;
+ return Promise.resolve().then(function () {
+ assert_false('resolved' in promise, 'The promise returned by "whenDefined" must not be resolved until the element is defined');
+ assert_false('rejected' in promise, 'The promise returned by "whenDefined" must not be rejected until the element is defined');
+ assert_equals(customElements.whenDefined('element-defined-after-whendefined'), promise,
+ '"whenDefined" must return the same unresolved promise before the custom element is defined');
+ customElements.define('element-defined-after-whendefined', ElementDefinedAfterWhenDefined);
+ assert_false('resolved' in promise, 'The promise returned by "whenDefined" must not be resolved until the end of the next microtask');
+ assert_false('rejected' in promise, 'The promise returned by "whenDefined" must not be rejected until the end of the next microtask');
+
+ promiseAfterDefine = customElements.whenDefined('element-defined-after-whendefined');
+ promiseAfterDefine.then(function (value) { promiseAfterDefine.resolved = value; }, function (value) { promiseAfterDefine.rejected = value; });
+ assert_not_equals(promiseAfterDefine, promise, '"whenDefined" must return a resolved promise once the custom element is defined');
+ assert_false('resolved' in promiseAfterDefine, 'The promise returned by "whenDefined" must not be resolved until the end of the next microtask');
+ assert_false('rejected' in promiseAfterDefine, 'The promise returned by "whenDefined" must not be rejected until the end of the next microtask');
+ }).then(function () {
+ assert_true('resolved' in promise, 'The promise returned by "whenDefined" must be resolved when a custom element is defined');
+ assert_equals(promise.resolved, ElementDefinedAfterWhenDefined,
+ 'The promise returned by "whenDefined" must be resolved with the constructor of the element when a custom element is defined');
+ assert_false('rejected' in promise, 'The promise returned by "whenDefined" must not be rejected when a custom element is defined');
+
+ assert_true('resolved' in promiseAfterDefine, 'The promise returned by "whenDefined" must be resolved when a custom element is defined');
+ assert_equals(promiseAfterDefine.resolved, ElementDefinedAfterWhenDefined,
+ 'The promise returned by "whenDefined" must be resolved with the constructor of the element when a custom element is defined');
+ assert_false('rejected' in promiseAfterDefine, 'The promise returned by "whenDefined" must not be rejected when a custom element is defined');
+ });
+}, 'A promise returned by customElements.whenDefined must be resolved by "define"');
+
+promise_test(function () {
+ class ResolvedCustomElement extends HTMLElement {};
+ customElements.define('resolved-custom-element', ResolvedCustomElement);
+ return customElements.whenDefined('resolved-custom-element').then(function (value) {
+ assert_equals(value, ResolvedCustomElement, 'The promise returned by "whenDefined" must resolve with the defined class');
+ });
+}, 'A promise returned by customElements.whenDefined must be resolved with the defined class');
+
+promise_test(function () {
+ var promise = customElements.whenDefined('not-resolved-yet-custom-element').then(function (value) {
+ assert_equals(value, NotResolvedYetCustomElement, 'The promise returned by "whenDefined" must resolve with the defined class once such class is defined');
+ });
+ class NotResolvedYetCustomElement extends HTMLElement {};
+ customElements.define('not-resolved-yet-custom-element', NotResolvedYetCustomElement);
+ return promise;
+}, 'A promise returned by customElements.whenDefined must be resolved with the defined class once such class is defined');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/Document-createElement-customized-builtins.html b/testing/web-platform/tests/custom-elements/Document-createElement-customized-builtins.html
new file mode 100644
index 0000000000..779b8affcb
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/Document-createElement-customized-builtins.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: document.createElement should create a customized builtin element with synchronous custom elements flag set</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/custom-elements-helpers.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+setup({allow_uncaught_exception:true});
+
+function assert_reports(expected, testFunction, message) {
+ var uncaughtError = null;
+ window.onerror = function (message, url, lineNumber, columnNumber, error) { uncaughtError = error; return true; }
+ testFunction();
+ if (typeof(expected) == 'string')
+ assert_equals(uncaughtError, expected, message);
+ else if (expected && 'name' in expected)
+ assert_equals(uncaughtError.name, expected.name, message);
+ else
+ assert_equals(uncaughtError, expected, message);
+ window.onerror = null;
+}
+
+test(function () {
+ class AutonomousCustomElement extends HTMLElement {};
+ class IsCustomElement extends HTMLElement {};
+
+ customElements.define('autonomous-custom-element', AutonomousCustomElement);
+ customElements.define('is-custom-element', IsCustomElement);
+
+ var instance = document.createElement('autonomous-custom-element', { is: 'is-custom-element'});
+
+ assert_true(instance instanceof AutonomousCustomElement);
+ assert_equals(instance.localName, 'autonomous-custom-element');
+ assert_equals(instance.namespaceURI, 'http://www.w3.org/1999/xhtml', 'A custom element HTML must use HTML namespace');
+
+ var instance2 = document.createElement('undefined-element', { is: 'is-custom-element'});
+ assert_false(instance2.matches(':defined'));
+ class DefinedLater extends HTMLElement {}
+ customElements.define('undefined-element', DefinedLater);
+ document.body.appendChild(instance2);
+ assert_true(instance2 instanceof DefinedLater);
+}, 'document.createElement must create an instance of autonomous custom elements when it has is attribute');
+
+test(() => {
+ class SuperP extends HTMLParagraphElement {}
+ customElements.define("super-p", SuperP, { extends: "p" });
+
+ const superP = document.createElement("p", { is: "super-p" });
+ assert_true(superP instanceof HTMLParagraphElement);
+ assert_true(superP instanceof SuperP);
+ assert_equals(superP.localName, "p");
+
+ const notSuperP = document.createElement("p", "super-p");
+ assert_true(notSuperP instanceof HTMLParagraphElement);
+ assert_false(notSuperP instanceof SuperP);
+ assert_equals(notSuperP.localName, "p");
+}, "document.createElement()'s second argument is to be ignored when it's a string");
+
+test(function () {
+ var exceptionToThrow = {name: 'exception thrown by a custom constructor'};
+ class ThrowCustomBuiltinElement extends HTMLDivElement {
+ constructor()
+ {
+ super();
+ if (exceptionToThrow)
+ throw exceptionToThrow;
+ }
+ };
+ customElements.define('throw-custom-builtin-element', ThrowCustomBuiltinElement, { extends: 'div' });
+
+ assert_throws_exactly(exceptionToThrow, function () { new ThrowCustomBuiltinElement; });
+ var instance;
+ assert_reports(exceptionToThrow, function () { instance = document.createElement('div', { is: 'throw-custom-builtin-element' }); });
+ assert_equals(instance.localName, 'div');
+ assert_true(instance instanceof HTMLDivElement);
+
+ exceptionToThrow = false;
+ var instance = document.createElement('div', { is: 'throw-custom-builtin-element' });
+ assert_true(instance instanceof ThrowCustomBuiltinElement);
+ assert_equals(instance.localName, 'div');
+
+}, 'document.createElement must report an exception thrown by a custom built-in element constructor');
+
+test(() => {
+ class MyElement extends HTMLDivElement {}
+
+ // createElement with unknown 'is' should not throw.
+ // https://github.com/w3c/webcomponents/issues/608
+ let div = document.createElement('div', { is: 'my-div' });
+ assert_false(div instanceof MyElement);
+ assert_false(div.hasAttribute('is'));
+
+ customElements.define('my-div', MyElement, { extends: 'div' });
+ document.body.appendChild(div);
+ assert_true(div instanceof MyElement, 'Undefined element is upgraded on connecting to a document');
+}, 'document.createElement with unknown "is" value should create "undefined" state element');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/Document-createElement-svg.svg b/testing/web-platform/tests/custom-elements/Document-createElement-svg.svg
new file mode 100644
index 0000000000..0b53bd830f
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/Document-createElement-svg.svg
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg:svg xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/1999/xhtml"
+ width="100%" height="100%" viewBox="0 0 800 600">
+<svg:title>document.createElement in SVG for custom elements</svg:title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script><![CDATA[
+test(() => {
+ class MyElement1 extends HTMLElement {}
+ customElements.define('my-element', MyElement1);
+ let element = document.createElement('my-element', {});
+ assert_false(element instanceof MyElement1, 'Autonomous custom element should not be created.');
+}, 'document.createElement() in SVG documents should not create autonomous custom elements.')
+
+test(() => {
+ class MyElement2 extends HTMLDivElement {}
+ customElements.define('my-div', MyElement2, { extends: 'div' });
+
+ let element = document.createElement('div', { is: 'my-div' });
+ assert_false(element instanceof MyElement2, 'Custom built-in element should not be created.');
+}, 'document.createElement() in SVG documents should not create custom built-in elements.')
+]]></script>
+</svg:svg>
diff --git a/testing/web-platform/tests/custom-elements/Document-createElement.html b/testing/web-platform/tests/custom-elements/Document-createElement.html
new file mode 100644
index 0000000000..7772f36e20
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/Document-createElement.html
@@ -0,0 +1,344 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: document.createElement should create an element with synchronous custom elements flag set</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="document.createElement should create an element with synchronous custom elements flag set">
+<link rel="help" content="https://dom.spec.whatwg.org/#dom-document-createelement">
+<link rel="help" content="https://dom.spec.whatwg.org/#concept-create-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/custom-elements-helpers.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+setup({allow_uncaught_exception:true});
+
+test(function () {
+ class MyCustomElement extends HTMLElement {};
+
+ assert_true(document.createElement('my-custom-element') instanceof HTMLElement);
+ assert_false(document.createElement('my-custom-element') instanceof MyCustomElement);
+
+ customElements.define('my-custom-element', MyCustomElement);
+ var instance = document.createElement('my-custom-element');
+ assert_true(instance instanceof MyCustomElement);
+ assert_equals(instance.localName, 'my-custom-element');
+ assert_equals(instance.namespaceURI, 'http://www.w3.org/1999/xhtml', 'A custom element HTML must use HTML namespace');
+
+}, 'document.createElement must create an instance of custom elements');
+
+function assert_reports(expected, testFunction, message) {
+ var uncaughtError = null;
+ window.onerror = function (message, url, lineNumber, columnNumber, error) { uncaughtError = error; return true; }
+ testFunction();
+ if (typeof(expected) == 'string')
+ assert_equals(uncaughtError, expected, message);
+ else if (expected && 'name' in expected)
+ assert_equals(uncaughtError.name, expected.name, message);
+ else
+ assert_equals(uncaughtError, expected, message);
+ window.onerror = null;
+}
+
+function assert_not_reports(testFunction, message) {
+ assert_reports(null, testFunction, message);
+}
+
+test(function () {
+ class ObjectCustomElement extends HTMLElement {
+ constructor()
+ {
+ return {foo: 'bar'};
+ }
+ };
+ customElements.define('object-custom-element', ObjectCustomElement);
+
+ var instance = new ObjectCustomElement;
+ assert_true(instance instanceof Object);
+ assert_equals(instance.foo, 'bar');
+
+ var instance;
+ assert_reports({name: 'TypeError'}, function () { instance = document.createElement('object-custom-element'); });
+ assert_equals(instance.localName, 'object-custom-element');
+ assert_true(instance instanceof HTMLUnknownElement);
+}, 'document.createElement must report a TypeError when the result of Construct is not a DOM node');
+
+test(function () {
+ class TextCustomElement extends HTMLElement {
+ constructor()
+ {
+ return document.createTextNode('hello');
+ }
+ };
+ customElements.define('text-custom-element', TextCustomElement);
+ assert_true(new TextCustomElement instanceof Text);
+ var instance;
+ assert_reports({name: 'TypeError'}, function () { instance = document.createElement('text-custom-element'); });
+ assert_equals(instance.localName, 'text-custom-element');
+ assert_true(instance instanceof HTMLUnknownElement);
+}, 'document.createElement must report a TypeError when the result of Construct is a TextNode');
+
+test(function () {
+ class ElementWithAttribute extends HTMLElement {
+ constructor()
+ {
+ super();
+ this.setAttribute('id', 'foo');
+ }
+ };
+ customElements.define('element-with-attribute', ElementWithAttribute);
+ assert_true(new ElementWithAttribute instanceof ElementWithAttribute);
+ var instance;
+ assert_reports({name: 'NotSupportedError'}, function () { instance = document.createElement('element-with-attribute'); });
+ assert_equals(instance.localName, 'element-with-attribute');
+ assert_true(instance instanceof HTMLUnknownElement);
+}, 'document.createElement must report a NotSupportedError when attribute is added by setAttribute during construction');
+
+test(function () {
+ class ElementWithAttrNode extends HTMLElement {
+ constructor()
+ {
+ super();
+ this.attributes.setNamedItem(document.createAttribute('title'));
+ }
+ };
+ customElements.define('element-with-attr-node', ElementWithAttrNode);
+ assert_true(new ElementWithAttrNode instanceof ElementWithAttrNode);
+ var instance;
+ assert_reports({name: 'NotSupportedError'}, function () { instance = document.createElement('element-with-attr-node'); });
+ assert_equals(instance.localName, 'element-with-attr-node');
+ assert_true(instance instanceof HTMLUnknownElement);
+}, 'document.createElement must report a NotSupportedError when attribute is added by attributes.setNamedItem during construction');
+
+test(function () {
+ class ElementWithNoAttributes extends HTMLElement {
+ constructor()
+ {
+ super();
+ this.attributes.setNamedItem(document.createAttribute('title'));
+ this.removeAttribute('title');
+ }
+ };
+ customElements.define('element-with-no-attiributes', ElementWithNoAttributes);
+ assert_true(new ElementWithNoAttributes instanceof ElementWithNoAttributes);
+ var instance;
+ assert_not_reports(function () { instance = document.createElement('element-with-no-attiributes'); });
+ assert_true(instance instanceof ElementWithNoAttributes);
+}, 'document.createElement must not report a NotSupportedError when attribute is added and removed during construction');
+
+test(function () {
+ class ElementWithChildText extends HTMLElement {
+ constructor()
+ {
+ super();
+ this.appendChild(document.createTextNode('hello'));
+ }
+ };
+ customElements.define('element-with-child-text', ElementWithChildText);
+ assert_true(new ElementWithChildText instanceof ElementWithChildText);
+ var instance;
+ assert_reports({name: 'NotSupportedError'}, function () { instance = document.createElement('element-with-child-text'); });
+ assert_equals(instance.localName, 'element-with-child-text');
+ assert_true(instance instanceof HTMLUnknownElement);
+}, 'document.createElement must report a NotSupportedError when a Text child is added during construction');
+
+test(function () {
+ class ElementWithChildComment extends HTMLElement {
+ constructor()
+ {
+ super();
+ this.appendChild(document.createComment('hello'));
+ }
+ };
+ customElements.define('element-with-child-comment', ElementWithChildComment);
+ assert_true(new ElementWithChildComment instanceof ElementWithChildComment);
+ var instance;
+ assert_reports({name: 'NotSupportedError'}, function () { instance = document.createElement('element-with-child-comment'); });
+ assert_equals(instance.localName, 'element-with-child-comment');
+ assert_true(instance instanceof HTMLUnknownElement);
+}, 'document.createElement must report a NotSupportedError when a Comment child is added during construction');
+
+test(function () {
+ class ElementWithChildElement extends HTMLElement {
+ constructor()
+ {
+ super();
+ this.appendChild(document.createElement('div'));
+ }
+ };
+ customElements.define('element-with-child-element', ElementWithChildElement);
+ assert_true(new ElementWithChildElement instanceof ElementWithChildElement);
+ var instance;
+ assert_reports({name: 'NotSupportedError'}, function () { instance = document.createElement('element-with-child-element'); });
+ assert_equals(instance.localName, 'element-with-child-element');
+ assert_true(instance instanceof HTMLUnknownElement);
+}, 'document.createElement must report a NotSupportedError when an element child is added during construction');
+
+test(function () {
+ class ElementWithNoChildElements extends HTMLElement {
+ constructor()
+ {
+ super();
+ this.appendChild(document.createElement('div'));
+ this.removeChild(this.firstChild);
+ }
+ };
+ customElements.define('element-with-no-child-elements', ElementWithNoChildElements);
+ var instance;
+ assert_not_reports(function () { instance = document.createElement('element-with-no-child-elements'); });
+ assert_true(instance instanceof ElementWithNoChildElements);
+}, 'document.createElement must not report a NotSupportedError when an element child is added and removed during construction');
+
+test(function () {
+ class ElementWithParent extends HTMLElement {
+ constructor()
+ {
+ super();
+ document.createElement('div').appendChild(this);
+ }
+ };
+ customElements.define('element-with-parent', ElementWithParent);
+ assert_true(new ElementWithParent instanceof ElementWithParent);
+ var instance;
+ assert_reports({name: 'NotSupportedError'}, function () { instance = document.createElement('element-with-parent'); });
+ assert_equals(instance.localName, 'element-with-parent');
+ assert_true(instance instanceof HTMLUnknownElement);
+}, 'document.createElement must report a NotSupportedError when the element gets inserted into another element during construction');
+
+test(function () {
+ class ElementWithNoParent extends HTMLElement {
+ constructor()
+ {
+ super();
+ document.createElement('div').appendChild(this);
+ this.parentNode.removeChild(this);
+ }
+ };
+ customElements.define('element-with-no-parent', ElementWithNoParent);
+ var instance;
+ assert_not_reports(function () { instance = document.createElement('element-with-no-parent'); });
+ assert_true(instance instanceof ElementWithNoParent);
+}, 'document.createElement must not report a NotSupportedError when the element is inserted and removed from another element during construction');
+
+document_types().forEach(function (entry, testNumber) {
+ if (entry.isOwner)
+ return;
+
+ var getDocument = entry.create;
+ var documentName = entry.name;
+
+ promise_test(function () {
+ return getDocument().then(function (doc) {
+ class ElementWithAdoptCall extends HTMLElement {
+ constructor()
+ {
+ super();
+ doc.adoptNode(this);
+ }
+ };
+ var name = 'element-with-adopt-call-' + testNumber;
+ customElements.define(name, ElementWithAdoptCall);
+ assert_true(new ElementWithAdoptCall instanceof ElementWithAdoptCall);
+ var instance;
+ assert_reports({name: 'NotSupportedError'}, function () { instance = document.createElement(name); });
+ assert_equals(instance.localName, name);
+ assert_true(instance instanceof HTMLUnknownElement);
+ });
+ }, `document.createElement must report a NotSupportedError when the element is adopted into a ${documentName} during construction`);
+
+ promise_test(function () {
+ return getDocument().then(function (doc) {
+ class ElementInsertedIntoAnotherDocument extends HTMLElement {
+ constructor()
+ {
+ super();
+ doc.documentElement.appendChild(this);
+ }
+ };
+ var name = 'element-inserted-into-another-document-' + testNumber;
+ customElements.define(name, ElementInsertedIntoAnotherDocument);
+ assert_true(new ElementInsertedIntoAnotherDocument instanceof ElementInsertedIntoAnotherDocument);
+ var instance;
+ assert_reports({name: 'NotSupportedError'}, function () { instance = document.createElement(name); });
+ assert_equals(instance.localName, name);
+ assert_true(instance instanceof HTMLUnknownElement);
+ });
+ }, `document.createElement must report a NotSupportedError when the element is inserted into a ${documentName} during construction`);
+
+ promise_test(function () {
+ return getDocument().then(function (doc) {
+ class ElementThatGetAdoptedBack extends HTMLElement {
+ constructor()
+ {
+ super();
+ doc.adoptNode(this);
+ document.adoptNode(this);
+ }
+ };
+ var name = 'element-that-get-adopted-back' + testNumber;
+ customElements.define(name, ElementThatGetAdoptedBack);
+ var instance;
+ assert_not_reports(function () { instance = document.createElement(name); });
+ assert_true(instance instanceof ElementThatGetAdoptedBack);
+ });
+ }, `document.createElement must not report a NotSupportedError when the element is adopted back from a ${documentName} during construction`);
+});
+
+test(function () {
+ class DivCustomElement extends HTMLElement {
+ constructor()
+ {
+ super();
+ return document.createElement('div');
+ }
+ };
+ customElements.define('div-custom-element', DivCustomElement);
+ assert_true(new DivCustomElement instanceof HTMLDivElement);
+ var instance;
+ assert_reports({name: 'NotSupportedError'}, function () { instance = document.createElement('div-custom-element'); });
+ assert_equals(instance.localName, 'div-custom-element');
+ assert_true(instance instanceof HTMLUnknownElement);
+}, 'document.createElement must report a NotSupportedError when the local name of the element does not match that of the custom element');
+
+test(function () {
+ var exceptionToThrow = {name: 'exception thrown by a custom constructor'};
+ class ThrowCustomElement extends HTMLElement {
+ constructor()
+ {
+ super();
+ if (exceptionToThrow)
+ throw exceptionToThrow;
+ }
+ };
+ customElements.define('throw-custom-element', ThrowCustomElement);
+
+ assert_throws_exactly(exceptionToThrow, function () { new ThrowCustomElement; });
+ var instance;
+ assert_reports(exceptionToThrow, function () { instance = document.createElement('throw-custom-element'); });
+ assert_equals(instance.localName, 'throw-custom-element');
+ assert_true(instance instanceof HTMLUnknownElement);
+
+ exceptionToThrow = false;
+ var instance = document.createElement('throw-custom-element');
+ assert_true(instance instanceof ThrowCustomElement);
+ assert_equals(instance.localName, 'throw-custom-element');
+
+}, 'document.createElement must report an exception thrown by a custom element constructor');
+
+test(() => {
+ class MyElement extends HTMLElement {
+ constructor() {
+ super();
+ this.foo = true;
+ }
+ }
+ customElements.define("my-element", MyElement);
+
+ const instance = document.createElement('my-element', undefined);
+ assert_true(instance.foo);
+}, 'document.createElement with undefined options value should be upgraded.');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/Document-createElementNS-customized-builtins.html b/testing/web-platform/tests/custom-elements/Document-createElementNS-customized-builtins.html
new file mode 100644
index 0000000000..ce7ab4aa22
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/Document-createElementNS-customized-builtins.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<title>Custom Elements: document.createElementNS should support custom elements</title>
+<link rel="help" content="https://dom.spec.whatwg.org/#concept-create-element">
+<link rel="help" content="https://dom.spec.whatwg.org/#internal-createelementns-steps">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+test(() => {
+ class MyBuiltinElement extends HTMLElement {};
+
+ customElements.define('my-builtin', MyBuiltinElement, { extends: 'address' });
+ let element = document.createElementNS('http://www.w3.org/1999/xhtml', 'p:address', { is: 'my-builtin'});
+ assert_true(element instanceof MyBuiltinElement);
+ assert_equals(element.prefix, 'p');
+ assert_false(element.hasAttribute('is'));
+}, 'builtin: document.createElementNS should create custom elements with prefixes.');
+
+test(() => {
+ class MyBuiltinElement2 extends HTMLElement {};
+
+ customElements.define('my-builtin2', MyBuiltinElement2, { extends: 'address'});
+ let element = document.createElementNS('urn:example', 'address', { is: 'my-builtin2' });
+ assert_false(element instanceof MyBuiltinElement2);
+ assert_false(element.hasAttribute('is'));
+}, 'builtin: document.createElementNS should check namespaces.');
+
+test(() => {
+ class SuperP extends HTMLParagraphElement {}
+ customElements.define("super-p", SuperP, { extends: "p" });
+
+ const superP = document.createElementNS("http://www.w3.org/1999/xhtml", "p", { is: "super-p" });
+ assert_true(superP instanceof HTMLParagraphElement);
+ assert_true(superP instanceof SuperP);
+ assert_equals(superP.localName, "p");
+
+ const notSuperP = document.createElementNS("http://www.w3.org/1999/xhtml", "p", "super-p");
+ assert_true(notSuperP instanceof HTMLParagraphElement);
+ assert_false(notSuperP instanceof SuperP);
+ assert_equals(notSuperP.localName, "p");
+}, "document.createElementNS()'s third argument is to be ignored when it's a string");
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/Document-createElementNS.html b/testing/web-platform/tests/custom-elements/Document-createElementNS.html
new file mode 100644
index 0000000000..bf937df0ba
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/Document-createElementNS.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<title>Custom Elements: document.createElementNS should support custom elements</title>
+<link rel="help" content="https://dom.spec.whatwg.org/#concept-create-element">
+<link rel="help" content="https://dom.spec.whatwg.org/#internal-createelementns-steps">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+test(() => {
+ class MyElement extends HTMLElement {};
+
+ customElements.define('my-autonomous', MyElement);
+ let element = document.createElementNS('http://www.w3.org/1999/xhtml', 'p:my-autonomous');
+ assert_true(element instanceof MyElement);
+ assert_equals(element.prefix, 'p');
+}, 'autonomous: document.createElementNS should create custom elements with prefixes.');
+
+test(() => {
+ class MyElement2 extends HTMLElement {};
+
+ customElements.define('my-autonomous2', MyElement2);
+ let element = document.createElementNS('urn:example', 'my-autonomous2');
+ assert_false(element instanceof MyElement2);
+}, 'autonomous: document.createElementNS should check namespaces.');
+
+test(() => {
+ const xhtmlNS = 'http://www.w3.org/1999/xhtml';
+ assert_false(document.createElementNS(xhtmlNS, 'x-foo') instanceof HTMLUnknownElement);
+ assert_false(document.createElementNS(xhtmlNS, 'x-foo', {}) instanceof HTMLUnknownElement);
+ assert_false((new Document()).createElementNS(xhtmlNS, 'x-foo') instanceof HTMLUnknownElement);
+ assert_false((new Document()).createElementNS(xhtmlNS, 'x-foo', {}) instanceof HTMLUnknownElement);
+}, 'autonomous: document.createElementNS should not create HTMLUnknownElement for a valid custom element name');
+
+test(() => {
+ class MyElement3 extends HTMLElement {};
+ customElements.define('my-autonomous3', MyElement3);
+
+ const instance = document.createElementNS('http://www.w3.org/1999/xhtml', 'my-autonomous3', undefined);
+ assert_true(instance instanceof MyElement3);
+}, 'autonomous: document.createElementNS with undefined options value should be upgraded.');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/ElementInternals-accessibility.html b/testing/web-platform/tests/custom-elements/ElementInternals-accessibility.html
new file mode 100644
index 0000000000..8a8f1c9aea
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/ElementInternals-accessibility.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+class TestElement extends HTMLElement {
+ constructor() {
+ super();
+ this._internals = this.attachInternals();
+ }
+
+ get internals() {
+ return this._internals;
+ }
+
+ set internals(val) {
+ throw "Can't set internals!";
+ }
+}
+customElements.define("test-element", TestElement);
+</script>
+
+<test-element id= "testElement"></test-element>
+
+<script>
+const element = document.getElementById("testElement");
+const properties = [
+ "role",
+ "ariaActiveDescendantElement",
+ "ariaAtomic",
+ "ariaAutoComplete",
+ "ariaBusy",
+ "ariaChecked",
+ "ariaColCount",
+ "ariaColIndex",
+ "ariaColSpan",
+ "ariaControlsElements",
+ "ariaCurrent",
+ "ariaDescribedByElements",
+ "ariaDetailsElements",
+ "ariaDisabled",
+ "ariaErrorMessageElements",
+ "ariaExpanded",
+ "ariaFlowToElements",
+ "ariaHasPopup",
+ "ariaHidden",
+ "ariaInvalid",
+ "ariaKeyShortcuts",
+ "ariaLabel",
+ "ariaLabelledByElements",
+ "ariaLevel",
+ "ariaLive",
+ "ariaModal",
+ "ariaMultiLine",
+ "ariaMultiSelectable",
+ "ariaOrientation",
+ "ariaOwnsElements",
+ "ariaPlaceholder",
+ "ariaPosInSet",
+ "ariaPressed",
+ "ariaReadOnly",
+ "ariaRelevant",
+ "ariaRequired",
+ "ariaRoleDescription",
+ "ariaRowCount",
+ "ariaRowIndex",
+ "ariaRowSpan",
+ "ariaSelected",
+ "ariaSort",
+ "ariaValueMax",
+ "ariaValueMin",
+ "ariaValueNow",
+ "ariaValueText"
+];
+for (const property of properties) {
+ test(() => {
+ assert_inherits(element.internals, property);
+ }, property + " is defined in ElementInternals");
+}
+test(() => assert_false('ariaErrorMessageElement' in element.internals), 'ariaErrorMessageElement is not defined in ElementInternals')
+</script>
diff --git a/testing/web-platform/tests/custom-elements/HTMLElement-attachInternals.html b/testing/web-platform/tests/custom-elements/HTMLElement-attachInternals.html
new file mode 100644
index 0000000000..43ea55a67e
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/HTMLElement-attachInternals.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="help" content="https://html.spec.whatwg.org/multipage/custom-elements.html#dom-attachinternals">
+<div id="container"></div>
+<script>
+test(() => {
+ class MyElement1 extends HTMLElement {
+ }
+ customElements.define('my-element1', MyElement1);
+ const container = document.querySelector('#container');
+
+ let element = new MyElement1();
+ assert_true(element.attachInternals() instanceof ElementInternals,
+ 'New - 1st call');
+ assert_throws_dom('NotSupportedError', () => { element.attachInternals(); },
+ 'New - 2nd call');
+
+ element = document.createElement('my-element1');
+ assert_true(element.attachInternals() instanceof ElementInternals,
+ 'createElement - 1st call');
+ assert_throws_dom('NotSupportedError', () => { element.attachInternals(); },
+ 'createElement - 2nd call');
+
+ container.innerHTML = '<my-element1></my-element1>';
+ assert_true(container.firstChild.attachInternals() instanceof ElementInternals,
+ 'Parser - 1st call');
+ assert_throws_dom('NotSupportedError', () => {
+ container.firstChild.attachInternals();
+ }, 'Parser - 2nd call');
+}, 'Successful attachInternals() and the second call.');
+
+test(() => {
+ class MyDiv extends HTMLDivElement {}
+ customElements.define('my-div', MyDiv, { extends: 'div' });
+ const customizedBuiltin = document.createElement('div', { is: 'my-div'});
+ assert_throws_dom('NotSupportedError', () => { customizedBuiltin.attachInternals() });
+}, 'attachInternals() throws a NotSupportedError if it is called for ' +
+ 'a customized built-in element');
+
+test(() => {
+ const builtin = document.createElement('div');
+ assert_throws_dom('NotSupportedError', () => { builtin.attachInternals() });
+
+ const doc = document.implementation.createDocument('foo', null);
+ const span = doc.appendChild(doc.createElementNS('http://www.w3.org/1999/xhtml', 'html:span'));
+ assert_true(span instanceof HTMLElement);
+ assert_throws_dom('NotSupportedError', () => { span.attachInternals(); });
+
+ const undefinedCustom = document.createElement('undefined-element');
+ assert_throws_dom('NotSupportedError', () => { undefinedCustom.attachInternals() });
+}, 'If a custom element definition for the local name of the element doesn\'t' +
+ ' exist, throw an NotSupportedError');
+
+test(() => {
+ class MyElement2 extends HTMLElement {
+ static get disabledFeatures() { return ['internals']; }
+ }
+ customElements.define('my-element2', MyElement2);
+ const container = document.querySelector('#container');
+
+ assert_throws_dom('NotSupportedError', () => {
+ (new MyElement2).attachInternals();
+ });
+ assert_throws_dom('NotSupportedError', () => {
+ document.createElement('my-element2').attachInternals();
+ });
+ assert_throws_dom('NotSupportedError', () => {
+ container.innerHTML = '<my-element2></my-element2>';
+ container.firstChild.attachInternals();
+ });
+
+ class MyElement3 extends HTMLElement {
+ static get disabledFeatures() { return ['INTERNALS']; }
+ }
+ customElements.define('my-element3', MyElement3);
+ assert_true((new MyElement3).attachInternals() instanceof ElementInternals);
+}, 'If a custom element definition for the local name of the element has ' +
+ 'disable internals flag, throw a NotSupportedError');
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/HTMLElement-constructor-customized-bulitins.html b/testing/web-platform/tests/custom-elements/HTMLElement-constructor-customized-bulitins.html
new file mode 100644
index 0000000000..9244dfe4ad
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/HTMLElement-constructor-customized-bulitins.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: HTMLElement must allow subclassing</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="HTMLElement must allow subclassing">
+<link rel="help" href="https://html.spec.whatwg.org/#html-element-constructors">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+test(function() {
+ class SomeCustomElement extends HTMLElement {};
+ var getCount = 0;
+ var countingProxy = new Proxy(SomeCustomElement, {
+ get: function(target, prop, receiver) {
+ if (prop == "prototype") {
+ ++getCount;
+ }
+ return Reflect.get(target, prop, receiver);
+ }
+ });
+ customElements.define("failure-counting-element-1", countingProxy,
+ { extends: "button" });
+ // define() gets the prototype of the constructor it's passed, so
+ // reset the counter.
+ getCount = 0;
+ assert_throws_js(TypeError,
+ function () { new countingProxy() },
+ "Should not be able to construct an HTMLElement named 'button'");
+ assert_equals(getCount, 0, "Should never have gotten .prototype");
+}, 'HTMLElement constructor must not get .prototype until it finishes its extends sanity checks, calling proxy constructor directly');
+
+test(function() {
+ class SomeCustomElement extends HTMLElement {};
+ var getCount = 0;
+ var countingProxy = new Proxy(SomeCustomElement, {
+ get: function(target, prop, receiver) {
+ if (prop == "prototype") {
+ ++getCount;
+ }
+ return Reflect.get(target, prop, receiver);
+ }
+ });
+ customElements.define("failure-counting-element-2", countingProxy,
+ { extends: "button" });
+ // define() gets the prototype of the constructor it's passed, so
+ // reset the counter.
+ getCount = 0;
+ assert_throws_js(TypeError,
+ function () { Reflect.construct(HTMLElement, [], countingProxy) },
+ "Should not be able to construct an HTMLElement named 'button'");
+ assert_equals(getCount, 0, "Should never have gotten .prototype");
+}, 'HTMLElement constructor must not get .prototype until it finishes its extends sanity checks, calling via Reflect');
+
+</script>
+</body>
+</html>
+
diff --git a/testing/web-platform/tests/custom-elements/HTMLElement-constructor.html b/testing/web-platform/tests/custom-elements/HTMLElement-constructor.html
new file mode 100644
index 0000000000..a387a1239a
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/HTMLElement-constructor.html
@@ -0,0 +1,186 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: HTMLElement must allow subclassing</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="HTMLElement must allow subclassing">
+<link rel="help" href="https://html.spec.whatwg.org/#html-element-constructors">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+test(function () {
+ customElements.define('html-custom-element', HTMLElement);
+ assert_throws_js(TypeError, function () { new HTMLElement(); });
+}, 'HTMLElement constructor must throw a TypeError when NewTarget is equal to itself');
+
+test(function () {
+ customElements.define('html-proxy-custom-element', new Proxy(HTMLElement, {}));
+ assert_throws_js(TypeError, function () { new HTMLElement(); });
+}, 'HTMLElement constructor must throw a TypeError when NewTarget is equal to itself via a Proxy object');
+
+test(function () {
+ class SomeCustomElement extends HTMLElement {};
+ assert_throws_js(TypeError, function () { new SomeCustomElement; });
+}, 'HTMLElement constructor must throw TypeError when it has not been defined by customElements.define');
+
+test(function () {
+ class SomeCustomElement extends HTMLParagraphElement {};
+ customElements.define('some-custom-element', SomeCustomElement);
+ assert_throws_js(TypeError, function () { new SomeCustomElement(); });
+}, 'Custom element constructor must throw TypeError when it does not extend HTMLElement');
+
+test(function () {
+ class SomeCustomButtonElement extends HTMLButtonElement {};
+ customElements.define('some-custom-button-element', SomeCustomButtonElement, { extends: "p" });
+ assert_throws_js(TypeError, function () { new SomeCustomButtonElement(); });
+}, 'Custom element constructor must throw TypeError when it does not extend the proper element interface');
+
+test(function () {
+ class CustomElementWithInferredTagName extends HTMLElement {};
+ customElements.define('inferred-name', CustomElementWithInferredTagName);
+
+ var instance = new CustomElementWithInferredTagName;
+ assert_true(instance instanceof Element, 'A custom element must inherit from Element');
+ assert_true(instance instanceof Node, 'A custom element must inherit from Node');
+ assert_equals(instance.localName, 'inferred-name');
+ assert_equals(instance.nodeName, 'INFERRED-NAME');
+ assert_equals(instance.namespaceURI, 'http://www.w3.org/1999/xhtml', 'A custom HTML element must use HTML namespace');
+
+ document.body.appendChild(instance);
+ assert_equals(document.body.lastChild, instance,
+ 'document.body.appendChild must be able to insert a custom element');
+ assert_equals(document.querySelector('inferred-name'), instance,
+ 'document.querySelector must be able to find a custom element by its tag name');
+
+}, 'HTMLElement constructor must infer the tag name from the element interface');
+
+test(function () {
+ class ConcreteCustomElement extends HTMLElement { };
+ class SubCustomElement extends ConcreteCustomElement { };
+ customElements.define('concrete-custom-element', ConcreteCustomElement);
+ customElements.define('sub-custom-element', SubCustomElement);
+
+ var instance = new ConcreteCustomElement();
+ assert_true(instance instanceof ConcreteCustomElement);
+ assert_false(instance instanceof SubCustomElement);
+ assert_equals(instance.localName, 'concrete-custom-element');
+ assert_equals(instance.nodeName, 'CONCRETE-CUSTOM-ELEMENT');
+
+ var instance = new SubCustomElement();
+ assert_true(instance instanceof ConcreteCustomElement);
+ assert_true(instance instanceof SubCustomElement);
+ assert_equals(instance.localName, 'sub-custom-element');
+ assert_equals(instance.nodeName, 'SUB-CUSTOM-ELEMENT');
+
+}, 'HTMLElement constructor must allow subclassing a custom element');
+
+test(function () {
+ class AbstractCustomElement extends HTMLElement { };
+ class ConcreteSubCustomElement extends AbstractCustomElement { };
+ customElements.define('concrete-sub-custom-element', ConcreteSubCustomElement);
+
+ var instance = new ConcreteSubCustomElement();
+ assert_true(instance instanceof AbstractCustomElement);
+ assert_true(instance instanceof ConcreteSubCustomElement);
+ assert_equals(instance.localName, 'concrete-sub-custom-element');
+ assert_equals(instance.nodeName, 'CONCRETE-SUB-CUSTOM-ELEMENT');
+
+}, 'HTMLElement constructor must allow subclassing an user-defined subclass of HTMLElement');
+
+test(function() {
+ class SomeCustomElement extends HTMLElement {};
+ var getCount = 0;
+ var countingProxy = new Proxy(SomeCustomElement, {
+ get: function(target, prop, receiver) {
+ if (prop == "prototype") {
+ ++getCount;
+ }
+ return Reflect.get(target, prop, receiver);
+ }
+ });
+ customElements.define("success-counting-element-1", countingProxy);
+ // define() gets the prototype of the constructor it's passed, so
+ // reset the counter.
+ getCount = 0;
+ var instance = new countingProxy();
+ assert_equals(getCount, 1, "Should have gotten .prototype once");
+ assert_true(instance instanceof countingProxy);
+ assert_true(instance instanceof HTMLElement);
+ assert_true(instance instanceof SomeCustomElement);
+ assert_equals(instance.localName, "success-counting-element-1");
+ assert_equals(instance.nodeName, "SUCCESS-COUNTING-ELEMENT-1");
+}, 'HTMLElement constructor must only get .prototype once, calling proxy constructor directly');
+
+test(function() {
+ class SomeCustomElement extends HTMLElement {};
+ var getCount = 0;
+ var countingProxy = new Proxy(SomeCustomElement, {
+ get: function(target, prop, receiver) {
+ if (prop == "prototype") {
+ ++getCount;
+ }
+ return Reflect.get(target, prop, receiver);
+ }
+ });
+ customElements.define("success-counting-element-2", countingProxy);
+ // define() gets the prototype of the constructor it's passed, so
+ // reset the counter.
+ getCount = 0;
+ var instance = Reflect.construct(HTMLElement, [], countingProxy);
+ assert_equals(getCount, 1, "Should have gotten .prototype once");
+ assert_true(instance instanceof countingProxy);
+ assert_true(instance instanceof HTMLElement);
+ assert_true(instance instanceof SomeCustomElement);
+ assert_equals(instance.localName, "success-counting-element-2");
+ assert_equals(instance.nodeName, "SUCCESS-COUNTING-ELEMENT-2");
+}, 'HTMLElement constructor must only get .prototype once, calling proxy constructor via Reflect');
+
+test(function() {
+ class SomeCustomElement {};
+ var getCount = 0;
+ var countingProxy = new Proxy(SomeCustomElement, {
+ get: function(target, prop, receiver) {
+ if (prop == "prototype") {
+ ++getCount;
+ }
+ return Reflect.get(target, prop, receiver);
+ }
+ });
+ customElements.define("success-counting-element-3", countingProxy);
+ // define() gets the prototype of the constructor it's passed, so
+ // reset the counter.
+ getCount = 0;
+ var instance = Reflect.construct(HTMLElement, [], countingProxy);
+ assert_equals(getCount, 1, "Should have gotten .prototype once");
+ assert_true(instance instanceof countingProxy);
+ assert_true(instance instanceof SomeCustomElement);
+ assert_equals(instance.localName, undefined);
+ assert_equals(instance.nodeName, undefined);
+}, 'HTMLElement constructor must only get .prototype once, calling proxy constructor via Reflect with no inheritance');
+
+test(function() {
+ class SomeCustomElement extends HTMLElement {};
+ var getCount = 0;
+ var countingProxy = new Proxy(SomeCustomElement, {
+ get: function(target, prop, receiver) {
+ if (prop == "prototype") {
+ ++getCount;
+ }
+ return Reflect.get(target, prop, receiver);
+ }
+ });
+
+ // Purposefully don't register it.
+ assert_throws_js(TypeError,
+ function () { Reflect.construct(HTMLElement, [], countingProxy) },
+ "Should not be able to construct an HTMLElement named 'button'");
+ assert_equals(getCount, 0, "Should never have gotten .prototype");
+}, 'HTMLElement constructor must not get .prototype until it finishes its registration sanity checks, calling via Reflect');
+</script>
+</body>
+</html>
+
diff --git a/testing/web-platform/tests/custom-elements/META.yml b/testing/web-platform/tests/custom-elements/META.yml
new file mode 100644
index 0000000000..e30f6fc97f
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/META.yml
@@ -0,0 +1,8 @@
+spec: https://html.spec.whatwg.org/multipage/custom-elements.html
+suggested_reviewers:
+ - snuggs
+ - domenic
+ - hayatoito
+ - kojiishi
+ - rniwa
+ - takayoshikochi
diff --git a/testing/web-platform/tests/custom-elements/adopted-callback.html b/testing/web-platform/tests/custom-elements/adopted-callback.html
new file mode 100644
index 0000000000..1ed4634dba
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/adopted-callback.html
@@ -0,0 +1,167 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: adoptedCallback</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="adoptedCallback must be enqueued whenever custom element is adopted into a new document">
+<link rel="help" href="https://w3c.github.io/webcomponents/spec/custom/#dfn-connected-callback">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/custom-elements-helpers.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+var calls = [];
+class MyCustomElement extends HTMLElement {
+ connectedCallback() { calls.push('connected'); }
+ adoptedCallback(oldDocument, newDocument) { calls.push('adopted'); calls.push(oldDocument); calls.push(newDocument); }
+ disconnectedCallback() { calls.push('disconnected'); }
+}
+customElements.define('my-custom-element', MyCustomElement);
+
+test(function () {
+ var instance = document.createElement('my-custom-element');
+ calls = [];
+ document.body.appendChild(instance);
+ assert_array_equals(calls, ['connected']);
+}, 'Inserting a custom element into the owner document must not enqueue and invoke adoptedCallback');
+
+document_types().forEach(function (entry) {
+ if (entry.isOwner)
+ return;
+
+ var documentName = entry.name;
+ var getDocument = entry.create;
+
+ promise_test(function () {
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ calls = [];
+ doc.documentElement.appendChild(instance);
+ assert_array_equals(calls, ['adopted', document, doc, 'connected']);
+ });
+ }, 'Inserting a custom element into ' + documentName + ' must enqueue and invoke adoptedCallback');
+
+ promise_test(function () {
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ document.body.appendChild(instance);
+ calls = [];
+ doc.documentElement.appendChild(instance);
+ assert_array_equals(calls, ['disconnected', 'adopted', document, doc, 'connected']);
+ });
+ }, 'Moving a custom element from the owner document into ' + documentName + ' must enqueue and invoke adoptedCallback');
+
+ promise_test(function () {
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ var parent = document.createElement('div');
+ parent.appendChild(instance);
+ calls = [];
+ doc.documentElement.appendChild(parent);
+ assert_array_equals(calls, ['adopted', document, doc, 'connected']);
+ });
+ }, 'Inserting an ancestor of custom element into ' + documentName + ' must enqueue and invoke adoptedCallback');
+
+ promise_test(function () {
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ var parent = document.createElement('div');
+ parent.appendChild(instance);
+ document.body.appendChild(parent);
+ calls = [];
+ doc.documentElement.appendChild(parent);
+ assert_array_equals(calls, ['disconnected', 'adopted', document, doc, 'connected']);
+ });
+ }, 'Moving an ancestor of custom element from the owner document into ' + documentName + ' must enqueue and invoke adoptedCallback');
+
+ promise_test(function () {
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ var host = doc.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ var shadowRoot = host.attachShadow({mode: 'closed'});
+ doc.documentElement.appendChild(host);
+
+ calls = [];
+ shadowRoot.appendChild(instance);
+ assert_array_equals(calls, ['adopted', document, doc, 'connected']);
+ });
+ }, 'Inserting a custom element into a shadow tree in ' + documentName + ' must enqueue and invoke adoptedCallback');
+
+ promise_test(function () {
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ var host = document.createElement('div');
+ var shadowRoot = host.attachShadow({mode: 'closed'});
+ shadowRoot.appendChild(instance);
+
+ calls = [];
+ doc.documentElement.appendChild(host);
+ assert_array_equals(calls, ['adopted', document, doc, 'connected']);
+ });
+ }, 'Inserting the shadow host of a custom element into ' + documentName + ' must enqueue and invoke adoptedCallback');
+
+ promise_test(function () {
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ var host = document.createElement('div');
+ var shadowRoot = host.attachShadow({mode: 'closed'});
+ shadowRoot.appendChild(instance);
+ document.body.appendChild(host);
+
+ calls = [];
+ doc.documentElement.appendChild(host);
+ assert_array_equals(calls, ['disconnected', 'adopted', document, doc, 'connected']);
+ });
+ }, 'Moving the shadow host of a custom element from the owner document into ' + documentName + ' must enqueue and invoke adoptedCallback');
+
+ promise_test(function () {
+ return getDocument().then(function (doc) {
+ const instance = document.createElement('my-custom-element');
+ const host = document.createElement('div');
+ const shadowRoot = host.attachShadow({mode: 'closed'});
+ shadowRoot.appendChild(instance);
+ document.body.appendChild(host);
+
+ calls = [];
+ doc.documentElement.appendChild(shadowRoot);
+ assert_array_equals(calls, ['disconnected', 'adopted', document, doc, 'connected']);
+ });
+ }, 'Moving the shadow host\'s shadow of a custom element from the owner document into ' + documentName + ' must enqueue and invoke adoptedCallback');
+
+ promise_test(function () {
+ return getDocument().then(function (doc) {
+ const instance = document.createElement('my-custom-element');
+ const template = document.createElement('template');
+ const templateContent = template.content;
+ templateContent.appendChild(instance);
+ document.body.appendChild(template);
+
+ calls = [];
+ doc.documentElement.appendChild(templateContent);
+ if (doc === templateContent.ownerDocument) {
+ assert_array_equals(calls, ['connected']);
+ } else {
+ assert_array_equals(calls, ['adopted', templateContent.ownerDocument, doc, 'connected']);
+ }
+ });
+ }, 'Moving the <template>\'s content of a custom element from the owner document into ' + documentName + ' must enqueue and invoke adoptedCallback');
+
+ promise_test(function () {
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ var host = doc.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ var shadowRoot = host.attachShadow({mode: 'closed'});
+
+ calls = [];
+ shadowRoot.appendChild(instance);
+ assert_array_equals(calls, ['adopted', document, doc]);
+ });
+ }, 'Inserting a custom element into a detached shadow tree that belongs to ' + documentName + ' must enqueue and invoke adoptedCallback');
+});
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/attribute-changed-callback.html b/testing/web-platform/tests/custom-elements/attribute-changed-callback.html
new file mode 100644
index 0000000000..db2a01196a
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/attribute-changed-callback.html
@@ -0,0 +1,271 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: attributeChangedCallback</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="attributeChangedCallback must be enqueued whenever custom element's attribute is added, changed or removed">
+<link rel="help" href="https://w3c.github.io/webcomponents/spec/custom/#dfn-attribute-changed-callback">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/custom-elements-helpers.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<parser-created-element title></parser-created-element>
+<script>
+
+var customElement = define_new_custom_element(['title', 'id', 'r']);
+
+test(function () {
+ const instance = document.createElement(customElement.name);
+ assert_array_equals(customElement.takeLog().types(), ['constructed']);
+
+ instance.setAttribute('title', 'foo');
+ assert_equals(instance.getAttribute('title'), 'foo');
+ var logEntries = customElement.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'title', oldValue: null, newValue: 'foo', namespace: null});
+
+ instance.removeAttribute('title');
+ assert_equals(instance.getAttribute('title'), null);
+ var logEntries = customElement.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'title', oldValue: 'foo', newValue: null, namespace: null});
+}, 'setAttribute and removeAttribute must enqueue and invoke attributeChangedCallback');
+
+test(function () {
+ var instance = document.createElement(customElement.name);
+ assert_array_equals(customElement.takeLog().types(), ['constructed']);
+
+ instance.setAttributeNS('http://www.w3.org/2000/svg', 'title', 'hello');
+ assert_equals(instance.getAttribute('title'), 'hello');
+ var logEntries = customElement.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'title', oldValue: null, newValue: 'hello', namespace: 'http://www.w3.org/2000/svg'});
+
+ instance.removeAttributeNS('http://www.w3.org/2000/svg', 'title');
+ assert_equals(instance.getAttribute('title'), null);
+ var logEntries = customElement.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'title', oldValue: 'hello', newValue: null, namespace: 'http://www.w3.org/2000/svg'});
+}, 'setAttributeNS and removeAttributeNS must enqueue and invoke attributeChangedCallback');
+
+test(function () {
+ var instance = document.createElement(customElement.name);
+ assert_array_equals(customElement.takeLog().types(), ['constructed']);
+
+ var attr = document.createAttribute('id');
+ attr.value = 'bar';
+ instance.setAttributeNode(attr);
+
+ assert_equals(instance.getAttribute('id'), 'bar');
+ var logEntries = customElement.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'id', oldValue: null, newValue: 'bar', namespace: null});
+
+ instance.removeAttributeNode(attr);
+ assert_equals(instance.getAttribute('id'), null);
+ var logEntries = customElement.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'id', oldValue: 'bar', newValue: null, namespace: null});
+}, 'setAttributeNode and removeAttributeNode must enqueue and invoke attributeChangedCallback for an HTML attribute');
+
+test(function () {
+ const instance = document.createElement(customElement.name);
+ assert_array_equals(customElement.takeLog().types(), ['constructed']);
+
+ const attr = document.createAttributeNS('http://www.w3.org/2000/svg', 'r');
+ attr.value = '100';
+ instance.setAttributeNode(attr);
+
+ assert_equals(instance.getAttribute('r'), '100');
+ var logEntries = customElement.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'r', oldValue: null, newValue: '100', namespace: 'http://www.w3.org/2000/svg'});
+
+ instance.removeAttributeNode(attr);
+ assert_equals(instance.getAttribute('r'), null);
+ var logEntries = customElement.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'r', oldValue: '100', newValue: null, namespace: 'http://www.w3.org/2000/svg'});
+}, 'setAttributeNode and removeAttributeNS must enqueue and invoke attributeChangedCallback for an SVG attribute');
+
+test(function () {
+ const instance = document.createElement(customElement.name);
+ assert_array_equals(customElement.takeLog().types(), ['constructed']);
+
+ instance.toggleAttribute('title', true);
+ assert_equals(instance.hasAttribute('title'), true);
+ var logEntries = customElement.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'title', oldValue: null, newValue: '', namespace: null});
+
+ instance.toggleAttribute('title');
+ assert_equals(instance.hasAttribute('title'), false);
+ var logEntries = customElement.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'title', oldValue: '', newValue: null, namespace: null});
+}, 'toggleAttribute must enqueue and invoke attributeChangedCallback');
+
+test(function () {
+ const callsToOld = [];
+ const callsToNew = [];
+ class CustomElement extends HTMLElement { }
+ CustomElement.prototype.attributeChangedCallback = function (...args) {
+ callsToOld.push(create_attribute_changed_callback_log(this, ...args));
+ }
+ CustomElement.observedAttributes = ['title'];
+ customElements.define('element-with-mutated-attribute-changed-callback', CustomElement);
+ CustomElement.prototype.attributeChangedCallback = function (...args) {
+ callsToNew.push(create_attribute_changed_callback_log(this, ...args));
+ }
+
+ const instance = document.createElement('element-with-mutated-attribute-changed-callback');
+ instance.setAttribute('title', 'hi');
+ assert_equals(instance.getAttribute('title'), 'hi');
+ assert_array_equals(callsToNew, []);
+ assert_equals(callsToOld.length, 1);
+ assert_attribute_log_entry(callsToOld[0], {name: 'title', oldValue: null, newValue: 'hi', namespace: null});
+}, 'Mutating attributeChangedCallback after calling customElements.define must not affect the callback being invoked');
+
+test(function () {
+ const calls = [];
+ class CustomElement extends HTMLElement {
+ attributeChangedCallback(...args) {
+ calls.push(create_attribute_changed_callback_log(this, ...args));
+ }
+ }
+ CustomElement.observedAttributes = ['title'];
+ customElements.define('element-not-observing-id-attribute', CustomElement);
+
+ const instance = document.createElement('element-not-observing-id-attribute');
+ assert_equals(calls.length, 0);
+ instance.setAttribute('title', 'hi');
+ assert_equals(calls.length, 1);
+ assert_attribute_log_entry(calls[0], {name: 'title', oldValue: null, newValue: 'hi', namespace: null});
+ instance.setAttribute('id', 'some');
+ assert_equals(calls.length, 1);
+ assert_attribute_log_entry(calls[0], {name: 'title', oldValue: null, newValue: 'hi', namespace: null});
+}, 'attributedChangedCallback must not be invoked when the observed attributes does not contain the attribute');
+
+test(function () {
+ const calls = [];
+ class CustomElement extends HTMLElement { }
+ CustomElement.prototype.attributeChangedCallback = function (...args) {
+ calls.push(create_attribute_changed_callback_log(this, ...args));
+ }
+ CustomElement.observedAttributes = ['title', 'lang'];
+ customElements.define('element-with-mutated-observed-attributes', CustomElement);
+ CustomElement.observedAttributes = ['title', 'id'];
+
+ const instance = document.createElement('element-with-mutated-observed-attributes');
+ instance.setAttribute('title', 'hi');
+ assert_equals(calls.length, 1);
+ assert_attribute_log_entry(calls[0], {name: 'title', oldValue: null, newValue: 'hi', namespace: null});
+
+ instance.setAttribute('id', 'some');
+ assert_equals(calls.length, 1);
+
+ instance.setAttribute('lang', 'en');
+ assert_equals(calls.length, 2);
+ assert_attribute_log_entry(calls[1], {name: 'lang', oldValue: null, newValue: 'en', namespace: null});
+}, 'Mutating observedAttributes after calling customElements.define must not affect the set of attributes for which attributedChangedCallback is invoked');
+
+test(function () {
+ var calls = [];
+ class CustomElement extends HTMLElement { }
+ CustomElement.prototype.attributeChangedCallback = function (...args) {
+ calls.push(create_attribute_changed_callback_log(this, ...args));
+ }
+ CustomElement.observedAttributes = { [Symbol.iterator]: function *() { yield 'lang'; yield 'style'; } };
+ customElements.define('element-with-generator-observed-attributes', CustomElement);
+
+ var instance = document.createElement('element-with-generator-observed-attributes');
+ instance.setAttribute('lang', 'en');
+ assert_equals(calls.length, 1);
+ assert_attribute_log_entry(calls[0], {name: 'lang', oldValue: null, newValue: 'en', namespace: null});
+
+ instance.setAttribute('lang', 'ja');
+ assert_equals(calls.length, 2);
+ assert_attribute_log_entry(calls[1], {name: 'lang', oldValue: 'en', newValue: 'ja', namespace: null});
+
+ instance.setAttribute('title', 'hello');
+ assert_equals(calls.length, 2);
+
+ instance.setAttribute('style', 'font-size: 2rem');
+ assert_equals(calls.length, 3);
+ assert_attribute_log_entry(calls[2], {name: 'style', oldValue: null, newValue: 'font-size: 2rem', namespace: null});
+}, 'attributedChangedCallback must be enqueued for attributes specified in a non-Array iterable observedAttributes');
+
+test(function () {
+ var calls = [];
+ class CustomElement extends HTMLElement { }
+ CustomElement.prototype.attributeChangedCallback = function (...args) {
+ calls.push(create_attribute_changed_callback_log(this, ...args));
+ }
+ CustomElement.observedAttributes = ['style'];
+ customElements.define('element-with-style-attribute-observation', CustomElement);
+
+ var instance = document.createElement('element-with-style-attribute-observation');
+ assert_equals(calls.length, 0);
+
+ instance.style.fontSize = '10px';
+ assert_equals(calls.length, 1);
+ assert_attribute_log_entry(calls[0], {name: 'style', oldValue: null, newValue: 'font-size: 10px;', namespace: null});
+
+ instance.style.fontSize = '20px';
+ assert_equals(calls.length, 2);
+ assert_attribute_log_entry(calls[1], {name: 'style', oldValue: 'font-size: 10px;', newValue: 'font-size: 20px;', namespace: null});
+
+}, 'attributedChangedCallback must be enqueued for style attribute change by mutating inline style declaration');
+
+test(function () {
+ var calls = [];
+ class CustomElement extends HTMLElement { }
+ CustomElement.prototype.attributeChangedCallback = function (...args) {
+ calls.push(create_attribute_changed_callback_log(this, ...args));
+ }
+ CustomElement.observedAttributes = ['title'];
+ customElements.define('element-with-no-style-attribute-observation', CustomElement);
+
+ var instance = document.createElement('element-with-no-style-attribute-observation');
+ assert_equals(calls.length, 0);
+ instance.style.fontSize = '10px';
+ assert_equals(calls.length, 0);
+ instance.title = 'hello';
+ assert_attribute_log_entry(calls[0], {name: 'title', oldValue: null, newValue: 'hello', namespace: null});
+}, 'attributedChangedCallback must not be enqueued when mutating inline style declaration if the style attribute is not observed');
+
+test(function () {
+ var calls = [];
+ class CustomElement extends HTMLElement { }
+ CustomElement.prototype.attributeChangedCallback = function (...args) {
+ calls.push(create_attribute_changed_callback_log(this, ...args));
+ }
+ CustomElement.observedAttributes = ['title'];
+ customElements.define('parser-created-element', CustomElement);
+ assert_attribute_log_entry(calls[0], {name: 'title', oldValue: null, newValue: '', namespace: null});
+}, 'Upgrading a parser created element must enqueue and invoke attributeChangedCallback for an HTML attribute');
+
+test(function () {
+ var calls = [];
+ class CustomElement extends HTMLElement { }
+ CustomElement.prototype.attributeChangedCallback = function (...args) {
+ calls.push(create_attribute_changed_callback_log(this, ...args));
+ }
+ CustomElement.observedAttributes = ['title'];
+ customElements.define('cloned-element-with-attribute', CustomElement);
+
+ var instance = document.createElement('cloned-element-with-attribute');
+ assert_equals(calls.length, 0);
+ instance.title = '';
+ assert_attribute_log_entry(calls[0], {name: 'title', oldValue: null, newValue: '', namespace: null});
+
+ calls = [];
+ var clone = instance.cloneNode(false);
+ assert_attribute_log_entry(calls[0], {name: 'title', oldValue: null, newValue: '', namespace: null});
+}, 'Upgrading a cloned element must enqueue and invoke attributeChangedCallback for an HTML attribute');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/builtin-coverage.html b/testing/web-platform/tests/custom-elements/builtin-coverage.html
new file mode 100644
index 0000000000..e3001a2c48
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/builtin-coverage.html
@@ -0,0 +1,182 @@
+<!DOCTYPE html>
+<html is="my-html">
+<head>
+<meta charset="utf-8">
+<meta name="help" content="https://html.spec.whatwg.org/multipage/custom-elements.html#element-definition">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body is="my-body">
+<div id="container"></div>
+<script>
+let testData = [
+ {tag: 'a', interface: 'HTMLAnchorElement'},
+ {tag: 'abbr', interface: 'HTMLElement'},
+ {tag: 'address', interface: 'HTMLElement'},
+ {tag: 'area', interface: 'HTMLAreaElement'},
+ {tag: 'article', interface: 'HTMLElement'},
+ {tag: 'aside', interface: 'HTMLElement'},
+ {tag: 'audio', interface: 'HTMLAudioElement'},
+ {tag: 'b', interface: 'HTMLElement'},
+ {tag: 'base', interface: 'HTMLBaseElement'},
+ {tag: 'bdi', interface: 'HTMLElement'},
+ {tag: 'bdo', interface: 'HTMLElement'},
+ {tag: 'blockquote', interface: 'HTMLQuoteElement'},
+ {tag: 'body', interface: 'HTMLBodyElement', parsing: 'document'},
+ {tag: 'br', interface: 'HTMLBRElement'},
+ {tag: 'button', interface: 'HTMLButtonElement'},
+ {tag: 'canvas', interface: 'HTMLCanvasElement'},
+ {tag: 'caption', interface: 'HTMLTableCaptionElement', parsing: 'table'},
+ {tag: 'cite', interface: 'HTMLElement'},
+ {tag: 'code', interface: 'HTMLElement'},
+ {tag: 'col', interface: 'HTMLTableColElement', parsing: 'table'},
+ {tag: 'colgroup', interface: 'HTMLTableColElement', parsing: 'table'},
+ {tag: 'data', interface: 'HTMLDataElement'},
+ {tag: 'dd', interface: 'HTMLElement'},
+ {tag: 'del', interface: 'HTMLModElement'},
+ {tag: 'details', interface: 'HTMLDetailsElement'},
+ {tag: 'dfn', interface: 'HTMLElement'},
+ {tag: 'div', interface: 'HTMLDivElement'},
+ {tag: 'dl', interface: 'HTMLDListElement'},
+ {tag: 'dt', interface: 'HTMLElement'},
+ {tag: 'em', interface: 'HTMLElement'},
+ {tag: 'embed', interface: 'HTMLEmbedElement'},
+ {tag: 'fieldset', interface: 'HTMLFieldSetElement'},
+ {tag: 'figcaption', interface: 'HTMLElement'},
+ {tag: 'figure', interface: 'HTMLElement'},
+ {tag: 'footer', interface: 'HTMLElement'},
+ {tag: 'form', interface: 'HTMLFormElement'},
+ {tag: 'h1', interface: 'HTMLHeadingElement'},
+ {tag: 'h2', interface: 'HTMLHeadingElement'},
+ {tag: 'h3', interface: 'HTMLHeadingElement'},
+ {tag: 'h4', interface: 'HTMLHeadingElement'},
+ {tag: 'h5', interface: 'HTMLHeadingElement'},
+ {tag: 'h6', interface: 'HTMLHeadingElement'},
+ {tag: 'header', interface: 'HTMLElement'},
+ {tag: 'hgroup', interface: 'HTMLElement'},
+ {tag: 'hr', interface: 'HTMLHRElement'},
+ {tag: 'html', interface: 'HTMLHtmlElement', parsing: 'document'},
+ {tag: 'i', interface: 'HTMLElement'},
+ {tag: 'iframe', interface: 'HTMLIFrameElement'},
+ {tag: 'img', interface: 'HTMLImageElement'},
+ {tag: 'input', interface: 'HTMLInputElement'},
+ {tag: 'ins', interface: 'HTMLModElement'},
+ {tag: 'kbd', interface: 'HTMLElement'},
+ {tag: 'label', interface: 'HTMLLabelElement'},
+ {tag: 'legend', interface: 'HTMLLegendElement'},
+ {tag: 'li', interface: 'HTMLLIElement'},
+ {tag: 'link', interface: 'HTMLLinkElement'},
+ {tag: 'main', interface: 'HTMLElement'},
+ {tag: 'map', interface: 'HTMLMapElement'},
+ {tag: 'mark', interface: 'HTMLElement'},
+ {tag: 'menu', interface: 'HTMLMenuElement'},
+ {tag: 'meta', interface: 'HTMLMetaElement'},
+ {tag: 'meter', interface: 'HTMLMeterElement'},
+ {tag: 'nav', interface: 'HTMLElement'},
+ {tag: 'noscript', interface: 'HTMLElement'},
+ {tag: 'object', interface: 'HTMLObjectElement'},
+ {tag: 'ol', interface: 'HTMLOListElement'},
+ {tag: 'optgroup', interface: 'HTMLOptGroupElement'},
+ {tag: 'option', interface: 'HTMLOptionElement'},
+ {tag: 'output', interface: 'HTMLOutputElement'},
+ {tag: 'p', interface: 'HTMLParagraphElement'},
+ {tag: 'param', interface: 'HTMLParamElement'},
+ {tag: 'picture', interface: 'HTMLPictureElement'},
+ {tag: 'pre', interface: 'HTMLPreElement'},
+ {tag: 'progress', interface: 'HTMLProgressElement'},
+ {tag: 'q', interface: 'HTMLQuoteElement'},
+ {tag: 'rp', interface: 'HTMLElement'},
+ {tag: 'rt', interface: 'HTMLElement'},
+ {tag: 'ruby', interface: 'HTMLElement'},
+ {tag: 's', interface: 'HTMLElement'},
+ {tag: 'samp', interface: 'HTMLElement'},
+ {tag: 'script', interface: 'HTMLScriptElement'},
+ {tag: 'section', interface: 'HTMLElement'},
+ {tag: 'select', interface: 'HTMLSelectElement'},
+ {tag: 'small', interface: 'HTMLElement'},
+ {tag: 'source', interface: 'HTMLSourceElement'},
+ {tag: 'span', interface: 'HTMLSpanElement'},
+ {tag: 'strong', interface: 'HTMLElement'},
+ {tag: 'style', interface: 'HTMLStyleElement'},
+ {tag: 'sub', interface: 'HTMLElement'},
+ {tag: 'summary', interface: 'HTMLElement'},
+ {tag: 'sup', interface: 'HTMLElement'},
+ {tag: 'table', interface: 'HTMLTableElement'},
+ {tag: 'tbody', interface: 'HTMLTableSectionElement', parsing: 'table'},
+ {tag: 'td', interface: 'HTMLTableCellElement', parsing: 'table'},
+ {tag: 'template', interface: 'HTMLTemplateElement'},
+ {tag: 'textarea', interface: 'HTMLTextAreaElement'},
+ {tag: 'tfoot', interface: 'HTMLTableSectionElement', parsing: 'table'},
+ {tag: 'th', interface: 'HTMLTableCellElement', parsing: 'table'},
+ {tag: 'thead', interface: 'HTMLTableSectionElement', parsing: 'table'},
+ {tag: 'time', interface: 'HTMLTimeElement'},
+ {tag: 'title', interface: 'HTMLTitleElement'},
+ {tag: 'tr', interface: 'HTMLTableRowElement', parsing: 'table'},
+ {tag: 'track', interface: 'HTMLTrackElement'},
+ {tag: 'u', interface: 'HTMLElement'},
+ {tag: 'ul', interface: 'HTMLUListElement'},
+ {tag: 'var', interface: 'HTMLElement'},
+ {tag: 'video', interface: 'HTMLVideoElement'},
+ {tag: 'wbr', interface: 'HTMLElement'},
+];
+// HTMLDataListElement isn't implemented by all major browsers yet.
+if (window.HTMLDataListElement) {
+ testData.push({tag: 'datalist', interface: 'HTMLDataListElement'});
+}
+// HTMLDialogElement isn't implemented by all major browsers yet.
+if (window.HTMLDialogElement) {
+ testData.push({tag: 'dialog', interface: 'HTMLDialogElement'});
+}
+// HTMLSlotElement isn't implemented by all major browsers yet.
+if (window.HTMLSlotElement) {
+ testData.push({tag: 'slot', interface: 'HTMLSlotElement'});
+}
+
+for (const t of testData) {
+ test(() => {
+ let name = 'my-' + t.tag;
+ let klass = eval(`(class extends ${t.interface} {})`);
+ customElements.define(name, klass, { extends: t.tag });
+
+ test(() => {
+ let customized = new klass();
+ assert_equals(customized.constructor, klass);
+ assert_equals(customized.cloneNode().constructor, klass,
+ 'Cloning a customized built-in element should succeed.');
+ }, `${t.tag}: Operator 'new' should instantiate a customized built-in element`);
+
+ test(() => {
+ let customized = document.createElement(t.tag, { is: name });
+ assert_equals(customized.constructor, klass);
+ assert_equals(customized.cloneNode().constructor, klass,
+ 'Cloning a customized built-in element should succeed.');
+ }, `${t.tag}: document.createElement() should instantiate a customized built-in element`);
+
+ if (t.parsing == 'document') {
+ let test = async_test(`${t.tag}: document parser should instantiate a customized built-in element`);
+ window.addEventListener('load', test.step_func_done(() => {
+ let customized = document.querySelector(t.tag);
+ assert_equals(customized.constructor, klass);
+ assert_equals(customized.cloneNode().constructor, klass,
+ 'Cloning a customized built-in element should succeed.');
+ }));
+ return;
+ }
+ test(() => {
+ let container = document.getElementById('container');
+ if (t.parsing == 'table') {
+ container.innerHTML = `<table><${t.tag} is="${name}" id="${name}">`;
+ } else {
+ container.innerHTML = `<${t.tag} is="${name}" id="${name}">`;
+ }
+ let customized = document.getElementById(name);
+ assert_equals(customized.constructor, klass);
+ assert_equals(customized.cloneNode().constructor, klass,
+ 'Cloning a customized built-in element should succeed.');
+ }, `${t.tag}: innerHTML should instantiate a customized built-in element`);
+
+ }, `${t.tag}: Define a customized built-in element`);
+}
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/connected-callbacks-html-fragment-parsing.html b/testing/web-platform/tests/custom-elements/connected-callbacks-html-fragment-parsing.html
new file mode 100644
index 0000000000..f24cc209c8
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/connected-callbacks-html-fragment-parsing.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: the HTML fragment parsing algorithm must not create a custom element synchronously</title>
+<meta name="author" title="Rob Buis" href="mailto:rbuis@igalia.com">
+<meta name="assert" content="The HTML fragment parsing algorithm must enqueue a custom element upgrade reaction instead of synchronously invoking its constructor">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/parsing.html#create-an-element-for-the-token">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/custom-elements-helpers.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+let iteration = 0;
+
+document_types().forEach(function (entry) {
+ var documentName = entry.name;
+ var getDocument = entry.create;
+ let calls = 0;
+
+ promise_test(function () {
+ class Parenter extends HTMLElement {
+ connectedCallback() {
+ const child = this.firstChild;
+ this.removeChild(child);
+ this.appendChild(child);
+ }
+ }
+ class Child extends HTMLElement {
+ connectedCallback() { calls++; }
+ }
+ iteration++;
+ let parenter = 'x-parenter' + iteration;
+ let child = 'x-child' + iteration;
+ customElements.define(parenter, Parenter);
+ customElements.define(child, Child);
+ return getDocument().then(function (doc) {
+ document.documentElement.innerHTML = `<${parenter}><${child}></${child}></${parenter}>`;
+ doc.documentElement.appendChild(document.documentElement.firstChild);
+ assert_equals(calls, 1);
+ });
+ }, `Inserting a custom element into ${documentName} using HTML fragment parsing must enqueue a custom element upgrade reaction, not synchronously invoke its constructor`);
+});
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/connected-callbacks-template.html b/testing/web-platform/tests/custom-elements/connected-callbacks-template.html
new file mode 100644
index 0000000000..ed404332db
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/connected-callbacks-template.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<!-- Regression test for https://github.com/jsdom/jsdom/issues/3290 -->
+
+<body>
+<script>
+"use strict";
+
+test(() => {
+ let innerConnectedCallbackCalled = false;
+ customElements.define("inner-element", class extends HTMLElement {
+ connectedCallback() {
+ innerConnectedCallbackCalled = true;
+ }
+ });
+
+ let outerConnectedCallbackCalled = false;
+ customElements.define("outer-element", class extends HTMLElement {
+ connectedCallback() {
+ const template = document.createElement("template");
+ template.innerHTML = "<inner-element></inner-element>";
+ this.appendChild(document.importNode(template.content, true));
+ outerConnectedCallbackCalled = true;
+ }
+ });
+
+ document.body.appendChild(document.createElement("outer-element"));
+ assert_true(innerConnectedCallbackCalled, "inner connectedCallback must be called");
+ assert_true(outerConnectedCallbackCalled, "outer connectedCallback must be called");
+}, "Nested custom element connectedCallback insertion involving a template DocumentFragment");
+</script>
diff --git a/testing/web-platform/tests/custom-elements/connected-callbacks.html b/testing/web-platform/tests/custom-elements/connected-callbacks.html
new file mode 100644
index 0000000000..d6e68262a8
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/connected-callbacks.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: connectedCallback</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="connectedCallback must be enqueued whenever custom element is inserted into a document">
+<link rel="help" href="https://w3c.github.io/webcomponents/spec/custom/#dfn-connected-callback">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/custom-elements-helpers.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+var calls = [];
+class MyCustomElement extends HTMLElement {
+ connectedCallback() { calls.push('connected', this); }
+ disconnectedCallback() { calls.push('disconnected', this); }
+}
+customElements.define('my-custom-element', MyCustomElement);
+
+document_types().forEach(function (entry) {
+ var documentName = entry.name;
+ var getDocument = entry.create;
+
+ promise_test(function () {
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ calls = [];
+ doc.documentElement.appendChild(instance);
+ assert_array_equals(calls, ['connected', instance]);
+ });
+ }, 'Inserting a custom element into ' + documentName + ' must enqueue and invoke connectedCallback');
+
+ promise_test(function () {
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ var parent = document.createElement('div');
+ parent.appendChild(instance);
+ calls = [];
+ doc.documentElement.appendChild(parent);
+ assert_array_equals(calls, ['connected', instance]);
+ });
+ }, 'Inserting an ancestor of custom element into ' + documentName + ' must enqueue and invoke connectedCallback');
+
+ promise_test(function () {
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ var host = doc.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ var shadowRoot = host.attachShadow({mode: 'closed'});
+ doc.documentElement.appendChild(host);
+
+ calls = [];
+ shadowRoot.appendChild(instance);
+ assert_array_equals(calls, ['connected', instance]);
+ });
+ }, 'Inserting a custom element into a shadow tree in ' + documentName + ' must enqueue and invoke connectedCallback');
+
+ promise_test(function () {
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ var host = doc.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ var shadowRoot = host.attachShadow({mode: 'closed'});
+ shadowRoot.appendChild(instance);
+
+ calls = [];
+ doc.documentElement.appendChild(host);
+ assert_array_equals(calls, ['connected', instance]);
+ });
+ }, 'Inserting the shadow host of a custom element into ' + documentName + ' must enqueue and invoke connectedCallback');
+
+ promise_test(function () {
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ var host = doc.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ var shadowRoot = host.attachShadow({mode: 'closed'});
+
+ calls = [];
+ shadowRoot.appendChild(instance);
+ assert_array_equals(calls, []);
+ });
+ }, 'Inserting a custom element into a detached shadow tree that belongs to ' + documentName + ' must not enqueue and invoke connectedCallback');
+});
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/cross-realm-callback-report-exception.html b/testing/web-platform/tests/custom-elements/cross-realm-callback-report-exception.html
new file mode 100644
index 0000000000..3067a7af9d
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/cross-realm-callback-report-exception.html
@@ -0,0 +1,83 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Exceptions raised in constructors / lifecycle callbacks are reported in their global objects</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<iframe></iframe>
+<iframe></iframe>
+<iframe></iframe>
+<script>
+setup({ allow_uncaught_exception: true });
+
+window.onerror = () => { onerrorCalls.push("top"); };
+frames[0].onerror = () => { onerrorCalls.push("frame0"); };
+frames[1].onerror = () => { onerrorCalls.push("frame1"); };
+frames[2].onerror = () => { onerrorCalls.push("frame2"); };
+
+const sourceThrowError = `throw new parent.frames[2].Error("PASS")`;
+
+test(t => {
+ window.onerrorCalls = [];
+
+ const XFoo = new frames[1].Function(sourceThrowError);
+ frames[0].customElements.define("x-foo-constructor", XFoo);
+
+ frames[0].document.createElement("x-foo-constructor");
+ assert_array_equals(onerrorCalls, ["frame1"]);
+}, "constructor");
+
+test(t => {
+ window.onerrorCalls = [];
+
+ const XFooConnected = class extends frames[0].HTMLElement {};
+ XFooConnected.prototype.connectedCallback = new frames[1].Function(sourceThrowError);
+ frames[0].customElements.define("x-foo-connected", XFooConnected);
+
+ const el = frames[0].document.createElement("x-foo-connected");
+ frames[0].document.body.append(el);
+
+ assert_array_equals(onerrorCalls, ["frame1"]);
+}, "connectedCallback");
+
+test(t => {
+ window.onerrorCalls = [];
+
+ const XFooDisconnected = class extends frames[0].HTMLElement {};
+ XFooDisconnected.prototype.disconnectedCallback = new frames[1].Function(sourceThrowError);
+ frames[0].customElements.define("x-foo-disconnected", XFooDisconnected);
+
+ const el = frames[0].document.createElement("x-foo-disconnected");
+ frames[0].document.body.append(el);
+ el.remove();
+
+ assert_array_equals(onerrorCalls, ["frame1"]);
+}, "disconnectedCallback");
+
+test(t => {
+ window.onerrorCalls = [];
+
+ const XFooAttributeChanged = class extends frames[0].HTMLElement {};
+ XFooAttributeChanged.observedAttributes = ["foo"];
+ XFooAttributeChanged.prototype.attributeChangedCallback = new frames[1].Function(sourceThrowError);
+ frames[0].customElements.define("x-foo-attribute-changed", XFooAttributeChanged);
+
+ const el = frames[0].document.createElement("x-foo-attribute-changed");
+ frames[0].document.body.append(el);
+ el.setAttribute("foo", "bar");
+
+ assert_array_equals(onerrorCalls, ["frame1"]);
+}, "attributeChangedCallback");
+
+test(t => {
+ window.onerrorCalls = [];
+
+ const XFooAdopted = class extends frames[0].HTMLElement {};
+ XFooAdopted.prototype.adoptedCallback = new frames[1].Function(sourceThrowError);
+ frames[0].customElements.define("x-foo-adopted", XFooAdopted);
+
+ const el = frames[0].document.createElement("x-foo-adopted");
+ document.body.append(el);
+
+ assert_array_equals(onerrorCalls, ["frame1"]);
+}, "adoptedCallback");
+</script>
diff --git a/testing/web-platform/tests/custom-elements/custom-element-reaction-queue.html b/testing/web-platform/tests/custom-elements/custom-element-reaction-queue.html
new file mode 100644
index 0000000000..246b15a0af
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/custom-element-reaction-queue.html
@@ -0,0 +1,190 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: Each element must have its own custom element reaction queue</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="Each element must have its own custom element reaction queue">
+<meta name="help" content="https://html.spec.whatwg.org/multipage/scripting.html#custom-element-reaction-queue">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/custom-elements-helpers.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+test_with_window(function (contentWindow) {
+ const contentDocument = contentWindow.document;
+ contentDocument.write('<test-element id="first-element">');
+ contentDocument.write('<test-element id="second-element">');
+
+ const element1 = contentDocument.getElementById('first-element');
+ const element2 = contentDocument.getElementById('second-element');
+ assert_equals(Object.getPrototypeOf(element1), contentWindow.HTMLElement.prototype);
+ assert_equals(Object.getPrototypeOf(element2), contentWindow.HTMLElement.prototype);
+
+ let log = [];
+ class TestElement extends contentWindow.HTMLElement {
+ constructor() {
+ super();
+ log.push(create_constructor_log(this));
+ }
+ connectedCallback(...args) {
+ log.push(create_connected_callback_log(this, ...args));
+ }
+ attributeChangedCallback(...args) {
+ log.push(create_attribute_changed_callback_log(this, ...args));
+ }
+ static get observedAttributes() { return ['id']; }
+ }
+ contentWindow.customElements.define('test-element', TestElement);
+ assert_equals(Object.getPrototypeOf(element1), TestElement.prototype);
+ assert_equals(Object.getPrototypeOf(element2), TestElement.prototype);
+
+ assert_equals(log.length, 6);
+ assert_constructor_log_entry(log[0], element1);
+ assert_attribute_log_entry(log[1], {name: 'id', oldValue: null, newValue: 'first-element', namespace: null});
+ assert_connected_log_entry(log[2], element1);
+ assert_constructor_log_entry(log[3], element2);
+ assert_attribute_log_entry(log[4], {name: 'id', oldValue: null, newValue: 'second-element', namespace: null});
+ assert_connected_log_entry(log[5], element2);
+}, 'Upgrading a custom element must invoke attributeChangedCallback and connectedCallback before start upgrading another element');
+
+test_with_window(function (contentWindow) {
+ const contentDocument = contentWindow.document;
+ contentDocument.write('<test-element>');
+
+ const element = contentDocument.querySelector('test-element');
+ assert_equals(Object.getPrototypeOf(element), contentWindow.HTMLElement.prototype);
+
+ let log = [];
+ class TestElement extends contentWindow.HTMLElement {
+ constructor() {
+ super();
+ this.id = "foo";
+ this.setAttribute('id', 'foo');
+ this.removeAttribute('id');
+ this.style.fontSize = '10px';
+ log.push(create_constructor_log(this));
+ }
+ connectedCallback(...args) {
+ log.push(create_connected_callback_log(this, ...args));
+ }
+ attributeChangedCallback(...args) {
+ log.push(create_attribute_changed_callback_log(this, ...args));
+ }
+ static get observedAttributes() { return ['id', 'style']; }
+ }
+ contentWindow.customElements.define('test-element', TestElement);
+ assert_equals(Object.getPrototypeOf(element), TestElement.prototype);
+
+ assert_equals(log.length, 2);
+ assert_constructor_log_entry(log[0], element);
+ assert_connected_log_entry(log[1], element);
+}, 'Upgrading a custom element must not invoke attributeChangedCallback for the attribute that is changed during upgrading');
+
+test_with_window(function (contentWindow) {
+ const contentDocument = contentWindow.document;
+ contentDocument.write('<test-element id="first-element">');
+ contentDocument.write('<test-element id="second-element">');
+
+ const element1 = contentDocument.getElementById('first-element');
+ const element2 = contentDocument.getElementById('second-element');
+ assert_equals(Object.getPrototypeOf(element1), contentWindow.HTMLElement.prototype);
+ assert_equals(Object.getPrototypeOf(element2), contentWindow.HTMLElement.prototype);
+
+ let log = [];
+ class TestElement extends contentWindow.HTMLElement {
+ constructor() {
+ super();
+ log.push(create_constructor_log(this));
+ if (this == element1) {
+ element2.setAttribute('title', 'hi');
+ element2.removeAttribute('title');
+ element2.setAttribute('class', 'foo');
+ }
+ }
+ connectedCallback(...args) {
+ log.push(create_connected_callback_log(this, ...args));
+ }
+ attributeChangedCallback(...args) {
+ log.push(create_attribute_changed_callback_log(this, ...args));
+ }
+ static get observedAttributes() { return ['id', 'class', 'title']; }
+ }
+ contentWindow.customElements.define('test-element', TestElement);
+ assert_equals(Object.getPrototypeOf(element1), TestElement.prototype);
+ assert_equals(Object.getPrototypeOf(element2), TestElement.prototype);
+
+ assert_equals(log.length, 7);
+ assert_constructor_log_entry(log[0], element1);
+ assert_attribute_log_entry(log[1], {name: 'id', oldValue: null, newValue: 'first-element', namespace: null});
+ assert_connected_log_entry(log[2], element1);
+ assert_constructor_log_entry(log[3], element2);
+ assert_attribute_log_entry(log[4], {name: 'id', oldValue: null, newValue: 'second-element', namespace: null});
+ assert_attribute_log_entry(log[5], {name: 'class', oldValue: null, newValue: 'foo', namespace: null});
+ assert_connected_log_entry(log[6], element2);
+}, 'Mutating a undefined custom element while upgrading a custom element must not enqueue or invoke reactions on the mutated element');
+
+test_with_window(function (contentWindow) {
+ let log = [];
+ let element1;
+ let element2;
+ class TestElement extends contentWindow.HTMLElement {
+ constructor() {
+ super();
+ log.push(create_constructor_log(this));
+ }
+ adoptedCallback(...args) {
+ log.push(create_adopted_callback_log(this, ...args));
+ if (this == element1)
+ element3.setAttribute('id', 'foo');
+ }
+ connectedCallback(...args) {
+ log.push(create_connected_callback_log(this, ...args));
+ }
+ attributeChangedCallback(...args) {
+ log.push(create_attribute_changed_callback_log(this, ...args));
+ }
+ static get observedAttributes() { return ['id', 'class']; }
+ }
+
+ contentWindow.customElements.define('test-element', TestElement);
+
+ let contentDocument = contentWindow.document;
+ element1 = contentDocument.createElement('test-element');
+ element2 = contentDocument.createElement('test-element');
+ element3 = contentDocument.createElement('test-element');
+ assert_equals(Object.getPrototypeOf(element1), TestElement.prototype);
+ assert_equals(Object.getPrototypeOf(element2), TestElement.prototype);
+ assert_equals(Object.getPrototypeOf(element3), TestElement.prototype);
+
+ assert_equals(log.length, 3);
+ assert_constructor_log_entry(log[0], element1);
+ assert_constructor_log_entry(log[1], element2);
+ assert_constructor_log_entry(log[2], element3);
+ log = [];
+
+ const container = contentDocument.createElement('div');
+ container.appendChild(element1);
+ container.appendChild(element2);
+ container.appendChild(element3);
+
+ const anotherDocument = document.implementation.createHTMLDocument();
+ anotherDocument.documentElement.appendChild(container);
+
+ assert_equals(log.length, 7);
+ assert_adopted_log_entry(log[0], element1);
+ assert_adopted_log_entry(log[1], element3);
+ assert_connected_log_entry(log[2], element3);
+ assert_attribute_log_entry(log[3], {name: 'id', oldValue: null, newValue: 'foo', namespace: null});
+ assert_connected_log_entry(log[4], element1);
+ assert_adopted_log_entry(log[5], element2);
+ assert_connected_log_entry(log[6], element2);
+
+}, 'Mutating another custom element inside adopted callback must invoke all pending callbacks on the mutated element');
+
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/custom-element-registry/define-customized-builtins.html b/testing/web-platform/tests/custom-elements/custom-element-registry/define-customized-builtins.html
new file mode 100644
index 0000000000..b691033871
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/custom-element-registry/define-customized-builtins.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<title>Custom Elements: Element Definition Customized Builtins</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div id="log"></div>
+<iframe id="iframe"></iframe>
+<script>
+'use strict';
+(() => {
+ // 2. If name is not a valid custom element name,
+ // then throw a SyntaxError and abort these steps.
+ let validCustomElementNames = [
+ // [a-z] (PCENChar)* '-' (PCENChar)*
+ // https://html.spec.whatwg.org/multipage/scripting.html#valid-custom-element-name
+ 'a-',
+ 'a-a',
+ 'aa-',
+ 'aa-a',
+ 'a-.-_',
+ 'a-0123456789',
+ 'a-\u6F22\u5B57', // Two CJK Unified Ideographs
+ 'a-\uD840\uDC0B', // Surrogate pair U+2000B
+ ];
+ let invalidCustomElementNames = [
+ undefined,
+ null,
+ '',
+ '-',
+ 'a',
+ 'input',
+ 'mycustomelement',
+ 'A',
+ 'A-',
+ '0-',
+ 'a-A',
+ 'a-Z',
+ 'A-a',
+ 'a-a\u00D7',
+ 'a-a\u3000',
+ 'a-a\uDB80\uDC00', // Surrogate pair U+F0000
+ // name must not be any of the hyphen-containing element names.
+ 'annotation-xml',
+ 'color-profile',
+ 'font-face',
+ 'font-face-src',
+ 'font-face-uri',
+ 'font-face-format',
+ 'font-face-name',
+ 'missing-glyph',
+ ];
+
+ const iframe = document.getElementById("iframe");
+ const testWindow = iframe.contentDocument.defaultView;
+ const customElements = testWindow.customElements;
+
+ // 9.1. If extends is a valid custom element name,
+ // then throw a NotSupportedError.
+ validCustomElementNames.forEach(name => {
+ test(() => {
+ assert_throws_dom('NOT_SUPPORTED_ERR', testWindow.DOMException, () => {
+ customElements.define('test-define-extend-valid-name', class {}, { extends: name });
+ });
+ }, `If extends is ${name}, should throw a NotSupportedError`);
+ });
+
+ // 9.2. If the element interface for extends and the HTML namespace is HTMLUnknownElement
+ // (e.g., if extends does not indicate an element definition in this specification),
+ // then throw a NotSupportedError.
+ [
+ // https://html.spec.whatwg.org/multipage/dom.html#elements-in-the-dom:htmlunknownelement
+ 'bgsound',
+ 'blink',
+ 'isindex',
+ 'multicol',
+ 'nextid',
+ 'spacer',
+ 'elementnametobeunknownelement',
+ ].forEach(name => {
+ test(() => {
+ assert_throws_dom('NOT_SUPPORTED_ERR', testWindow.DOMException, () => {
+ customElements.define('test-define-extend-' + name, class {}, { extends: name });
+ });
+ }, `If extends is ${name}, should throw a NotSupportedError`);
+ });
+})();
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/custom-element-registry/define.html b/testing/web-platform/tests/custom-elements/custom-element-registry/define.html
new file mode 100644
index 0000000000..d3d923bd91
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/custom-element-registry/define.html
@@ -0,0 +1,214 @@
+<!DOCTYPE html>
+<title>Custom Elements: Element definition</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div id="log"></div>
+<iframe id="iframe"></iframe>
+<script>
+'use strict';
+(() => {
+ // Element definition
+ // https://html.spec.whatwg.org/multipage/scripting.html#element-definition
+
+ // Use window from iframe to isolate the test.
+ const iframe = document.getElementById("iframe");
+ const testWindow = iframe.contentDocument.defaultView;
+ const customElements = testWindow.customElements;
+
+ let testable = false;
+ test(() => {
+ assert_true('customElements' in testWindow, '"window.customElements" exists');
+ assert_true('define' in customElements, '"window.customElements.define" exists');
+ testable = true;
+ }, '"window.customElements.define" should exists');
+ if (!testable)
+ return;
+
+ const expectTypeError = testWindow.TypeError;
+ // Following errors are DOMException, not JavaScript errors.
+ const expectSyntaxError = 'SYNTAX_ERR';
+ const expectNotSupportedError = 'NOT_SUPPORTED_ERR';
+
+ // 1. If IsConstructor(constructor) is false,
+ // then throw a TypeError and abort these steps.
+ test(() => {
+ assert_throws_js(expectTypeError, () => {
+ customElements.define();
+ });
+ }, 'If no arguments, should throw a TypeError');
+ test(() => {
+ assert_throws_js(expectTypeError, () => {
+ customElements.define('test-define-one-arg');
+ });
+ }, 'If one argument, should throw a TypeError');
+ [
+ [ 'undefined', undefined ],
+ [ 'null', null ],
+ [ 'object', {} ],
+ [ 'string', 'string' ],
+ [ 'arrow function', () => {} ], // IsConstructor returns false for arrow functions
+ [ 'method', ({ m() { } }).m ], // IsConstructor returns false for methods
+ ].forEach(t => {
+ test(() => {
+ assert_throws_js(expectTypeError, () => {
+ customElements.define(`test-define-constructor-${t[0]}`, t[1]);
+ });
+ }, `If constructor is ${t[0]}, should throw a TypeError`);
+ });
+
+ // 2. If name is not a valid custom element name,
+ // then throw a SyntaxError and abort these steps.
+ let validCustomElementNames = [
+ // [a-z] (PCENChar)* '-' (PCENChar)*
+ // https://html.spec.whatwg.org/multipage/scripting.html#valid-custom-element-name
+ 'a-',
+ 'a-a',
+ 'aa-',
+ 'aa-a',
+ 'a-.-_',
+ 'a-0123456789',
+ 'a-\u6F22\u5B57', // Two CJK Unified Ideographs
+ 'a-\uD840\uDC0B', // Surrogate pair U+2000B
+ ];
+ let invalidCustomElementNames = [
+ undefined,
+ null,
+ '',
+ '-',
+ 'a',
+ 'input',
+ 'mycustomelement',
+ 'A',
+ 'A-',
+ '0-',
+ 'a-A',
+ 'a-Z',
+ 'A-a',
+ 'a-a\u00D7',
+ 'a-a\u3000',
+ 'a-a\uDB80\uDC00', // Surrogate pair U+F0000
+ // name must not be any of the hyphen-containing element names.
+ 'annotation-xml',
+ 'color-profile',
+ 'font-face',
+ 'font-face-src',
+ 'font-face-uri',
+ 'font-face-format',
+ 'font-face-name',
+ 'missing-glyph',
+ ];
+ validCustomElementNames.forEach(name => {
+ test(() => {
+ customElements.define(name, class {});
+ }, `Element names: defining an element named ${name} should succeed`);
+ });
+ invalidCustomElementNames.forEach(name => {
+ test(() => {
+ assert_throws_dom(expectSyntaxError, testWindow.DOMException, () => {
+ customElements.define(name, class {});
+ });
+ }, `Element names: defining an element named ${name} should throw a SyntaxError`);
+ });
+
+ // 3. If this CustomElementRegistry contains an entry with name name,
+ // then throw a NotSupportedError and abort these steps.
+ test(() => {
+ customElements.define('test-define-dup-name', class {});
+ assert_throws_dom(expectNotSupportedError, testWindow.DOMException, () => {
+ customElements.define('test-define-dup-name', class {});
+ });
+ }, 'If the name is already defined, should throw a NotSupportedError');
+
+ // 5. If this CustomElementRegistry contains an entry with constructor constructor,
+ // then throw a NotSupportedError and abort these steps.
+ test(() => {
+ class TestDupConstructor {};
+ customElements.define('test-define-dup-constructor', TestDupConstructor);
+ assert_throws_dom(expectNotSupportedError, testWindow.DOMException, () => {
+ customElements.define('test-define-dup-ctor2', TestDupConstructor);
+ });
+ }, 'If the constructor is already defined, should throw a NotSupportedError');
+
+ // 12.1. Let prototype be Get(constructor, "prototype"). Rethrow any exceptions.
+ const err = new Error('check this is rethrown');
+ err.name = 'rethrown';
+ function assert_rethrown(func, description) {
+ assert_throws_exactly(err, func, description);
+ }
+ function throw_rethrown_error() {
+ throw err;
+ }
+ test(() => {
+ // Hack for prototype to throw while IsConstructor is true.
+ const BadConstructor = (function () { }).bind({});
+ Object.defineProperty(BadConstructor, 'prototype', {
+ get() { throw_rethrown_error(); }
+ });
+ assert_rethrown(() => {
+ customElements.define('test-define-constructor-prototype-rethrow', BadConstructor);
+ });
+ }, 'If constructor.prototype throws, should rethrow');
+
+ // 12.2. If Type(prototype) is not Object,
+ // then throw a TypeError exception.
+ test(() => {
+ const c = (function () { }).bind({}); // prototype is undefined.
+ assert_throws_js(expectTypeError, () => {
+ customElements.define('test-define-constructor-prototype-undefined', c);
+ });
+ }, 'If Type(constructor.prototype) is undefined, should throw a TypeError');
+ test(() => {
+ function c() {};
+ c.prototype = 'string';
+ assert_throws_js(expectTypeError, () => {
+ customElements.define('test-define-constructor-prototype-string', c);
+ });
+ }, 'If Type(constructor.prototype) is string, should throw a TypeError');
+
+ // 12.3. Let lifecycleCallbacks be a map with the four keys "connectedCallback",
+ // "disconnectedCallback", "adoptedCallback", and "attributeChangedCallback",
+ // each of which belongs to an entry whose value is null.
+ // 12.4. For each of the four keys callbackName in lifecycleCallbacks:
+ // 12.4.1. Let callbackValue be Get(prototype, callbackName). Rethrow any exceptions.
+ // 12.4.2. If callbackValue is not undefined, then set the value of the entry in
+ // lifecycleCallbacks with key callbackName to the result of converting callbackValue
+ // to the Web IDL Function callback type. Rethrow any exceptions from the conversion.
+ [
+ 'connectedCallback',
+ 'disconnectedCallback',
+ 'adoptedCallback',
+ 'attributeChangedCallback',
+ ].forEach(name => {
+ test(() => {
+ class C {
+ get [name]() { throw_rethrown_error(); }
+ }
+ assert_rethrown(() => {
+ customElements.define(`test-define-${name.toLowerCase()}-rethrow`, C);
+ });
+ }, `If constructor.prototype.${name} throws, should rethrow`);
+
+ [
+ { name: 'undefined', value: undefined, success: true },
+ { name: 'function', value: function () { }, success: true },
+ { name: 'null', value: null, success: false },
+ { name: 'object', value: {}, success: false },
+ { name: 'integer', value: 1, success: false },
+ ].forEach(data => {
+ test(() => {
+ class C { };
+ C.prototype[name] = data.value;
+ if (data.success) {
+ customElements.define(`test-define-${name.toLowerCase()}-${data.name}`, C);
+ } else {
+ assert_throws_js(expectTypeError, () => {
+ customElements.define(`test-define-${name.toLowerCase()}-${data.name}`, C);
+ });
+ }
+ }, `If constructor.prototype.${name} is ${data.name}, should ${data.success ? 'succeed' : 'throw a TypeError'}`);
+ });
+ });
+})();
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/custom-element-registry/per-global.html b/testing/web-platform/tests/custom-elements/custom-element-registry/per-global.html
new file mode 100644
index 0000000000..3570dcf811
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/custom-element-registry/per-global.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Custom Elements: CustomElementRegistry is per global</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#custom-elements-api">
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+<script src="/common/object-association.js"></script>
+
+<body>
+<script>
+"use strict";
+testIsPerWindow("customElements");
+</script>
diff --git a/testing/web-platform/tests/custom-elements/custom-element-registry/upgrade.html b/testing/web-platform/tests/custom-elements/custom-element-registry/upgrade.html
new file mode 100644
index 0000000000..e020d95a57
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/custom-element-registry/upgrade.html
@@ -0,0 +1,157 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>customElements.upgrade()</title>
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/#dom-customelementregistry-upgrade">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+"use strict";
+
+test(() => {
+ const el = document.createElement("spider-man");
+
+ class SpiderMan extends HTMLElement {}
+ customElements.define("spider-man", SpiderMan);
+
+ assert_false(el instanceof SpiderMan, "The element must not yet be upgraded");
+
+ customElements.upgrade(el);
+ assert_true(el instanceof SpiderMan, "The element must now be upgraded");
+}, "Upgrading an element directly (example from the spec)");
+
+test(() => {
+ const el1 = document.createElement("element-a-1");
+ const el2 = document.createElement("element-a-2");
+ const container = document.createElement("div");
+ container.appendChild(el1);
+ container.appendChild(el2);
+
+ class Element1 extends HTMLElement {}
+ class Element2 extends HTMLElement {}
+ customElements.define("element-a-1", Element1);
+ customElements.define("element-a-2", Element2);
+
+ assert_false(el1 instanceof Element1, "element 1 must not yet be upgraded");
+ assert_false(el2 instanceof Element2, "element 2 must not yet be upgraded");
+
+ customElements.upgrade(container);
+ assert_true(el1 instanceof Element1, "element 1 must now be upgraded");
+ assert_true(el2 instanceof Element2, "element 2 must now be upgraded");
+}, "Two elements as children of the upgraded node");
+
+test(() => {
+ const el1 = document.createElement("element-b-1");
+ const el2 = document.createElement("element-b-2");
+ const container = document.createElement("div");
+ const subContainer = document.createElement("span");
+ const subSubContainer = document.createElement("span");
+ container.appendChild(subContainer);
+ subContainer.appendChild(el1);
+ subContainer.appendChild(subSubContainer);
+ subSubContainer.appendChild(el2);
+
+ class Element1 extends HTMLElement {}
+ class Element2 extends HTMLElement {}
+ customElements.define("element-b-1", Element1);
+ customElements.define("element-b-2", Element2);
+
+ assert_false(el1 instanceof Element1, "element 1 must not yet be upgraded");
+ assert_false(el2 instanceof Element2, "element 2 must not yet be upgraded");
+
+ customElements.upgrade(container);
+ assert_true(el1 instanceof Element1, "element 1 must now be upgraded");
+ assert_true(el2 instanceof Element2, "element 2 must now be upgraded");
+}, "Two elements as descendants of the upgraded node");
+
+test(() => {
+ const el1 = document.createElement("element-d-1");
+ const el2 = document.createElement("element-d-2");
+
+ const container = document.createElement("div");
+ const subContainer = document.createElement("span");
+ subContainer.attachShadow({ mode: "open" });
+ const subSubContainer = document.createElement("span");
+ subSubContainer.attachShadow({ mode: "open" });
+
+ container.appendChild(subContainer);
+ subContainer.shadowRoot.appendChild(el1);
+ subContainer.shadowRoot.appendChild(subSubContainer);
+ subSubContainer.shadowRoot.appendChild(el2);
+
+ class Element1 extends HTMLElement {}
+ class Element2 extends HTMLElement {}
+ customElements.define("element-d-1", Element1);
+ customElements.define("element-d-2", Element2);
+
+ assert_false(el1 instanceof Element1, "element 1 must not yet be upgraded");
+ assert_false(el2 instanceof Element2, "element 2 must not yet be upgraded");
+
+ customElements.upgrade(container);
+ assert_true(el1 instanceof Element1, "element 1 must now be upgraded");
+ assert_true(el2 instanceof Element2, "element 2 must now be upgraded");
+}, "Two elements as shadow-including descendants (and not descendants) of the upgraded node");
+
+test(() => {
+ const template = document.createElement("template");
+ template.innerHTML = `
+ <div>
+ <element-c-1></element-c-1>
+ <element-c-2>
+ <element-c-3></element-c-3>
+ <span>
+ <element-c-4></element-c-4>
+ </span>
+ </element-c-2>
+ </div>
+ <element-c-5></element-c-5>
+ `;
+
+ // This code feels repetitive but I tried to make it use loops and it became harder to see the correctness.
+
+ const el1 = template.content.querySelector("element-c-1");
+ const el2 = template.content.querySelector("element-c-2");
+ const el3 = template.content.querySelector("element-c-3");
+ const el4 = template.content.querySelector("element-c-4");
+ const el5 = template.content.querySelector("element-c-5");
+
+ class Element1 extends HTMLElement {}
+ class Element2 extends HTMLElement {}
+ class Element3 extends HTMLElement {}
+ class Element4 extends HTMLElement {}
+ class Element5 extends HTMLElement {}
+
+ customElements.define("element-c-1", Element1);
+ customElements.define("element-c-2", Element2);
+ customElements.define("element-c-3", Element3);
+ customElements.define("element-c-4", Element4);
+ customElements.define("element-c-5", Element5);
+
+ assert_false(el1 instanceof Element1, "element 1 must not yet be upgraded");
+ assert_false(el2 instanceof Element2, "element 2 must not yet be upgraded");
+ assert_false(el3 instanceof Element3, "element 3 must not yet be upgraded");
+ assert_false(el4 instanceof Element4, "element 4 must not yet be upgraded");
+ assert_false(el5 instanceof Element5, "element 5 must not yet be upgraded");
+
+ customElements.upgrade(template);
+
+ assert_false(el1 instanceof Element1, "element 1 must not yet be upgraded despite upgrading the template");
+ assert_false(el2 instanceof Element2, "element 2 must not yet be upgraded despite upgrading the template");
+ assert_false(el3 instanceof Element3, "element 3 must not yet be upgraded despite upgrading the template");
+ assert_false(el4 instanceof Element4, "element 4 must not yet be upgraded despite upgrading the template");
+ assert_false(el5 instanceof Element5, "element 5 must not yet be upgraded despite upgrading the template");
+
+ customElements.upgrade(template.content);
+
+ // Template contents owner documents don't have a browsing context, so
+ // https://html.spec.whatwg.org/multipage/custom-elements.html#look-up-a-custom-element-definition does not find any
+ // custom element definition.
+ assert_false(el1 instanceof Element1, "element 1 must still not be upgraded after upgrading the template contents");
+ assert_false(el2 instanceof Element2, "element 2 must still not be upgraded after upgrading the template contents");
+ assert_false(el3 instanceof Element3, "element 3 must still not be upgraded after upgrading the template contents");
+ assert_false(el4 instanceof Element4, "element 4 must still not be upgraded after upgrading the template contents");
+ assert_false(el5 instanceof Element5, "element 5 must still not be upgraded after upgrading the template contents");
+}, "Elements inside a template contents DocumentFragment node");
+</script>
diff --git a/testing/web-platform/tests/custom-elements/customized-built-in-constructor-exceptions.html b/testing/web-platform/tests/custom-elements/customized-built-in-constructor-exceptions.html
new file mode 100644
index 0000000000..fbc1a6fd87
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/customized-built-in-constructor-exceptions.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<title>Customized built-in element constructor behavior</title>
+<meta name='author' href='mailto:masonf@chromium.org'>
+<link rel='help' href='https://dom.spec.whatwg.org/#concept-create-element'>
+<script src='/resources/testharness.js'></script>
+<script src='/resources/testharnessreport.js'></script>
+
+<script>
+setup({allow_uncaught_exception : true});
+
+class MyCustomParagraph extends HTMLParagraphElement {
+ constructor() {
+ super();
+ this.textContent = 'PASS';
+ }
+}
+customElements.define('custom-p', MyCustomParagraph, { extends: 'p' });
+</script>
+<p id=targetp is='custom-p'></p>
+<script>
+test(t => {
+ let target = document.getElementById('targetp');
+ assert_true(!!target);
+ assert_equals(target.localName, 'p');
+ assert_true(target instanceof MyCustomParagraph);
+ assert_true(target instanceof HTMLParagraphElement);
+ assert_equals(target.childNodes.length, 1);
+ assert_equals(target.textContent, 'PASS');
+}, 'Appending children in customized built-in constructor should work');
+</script>
+
+
+<script>
+class MyCustomVideo extends HTMLVideoElement {
+ constructor() {
+ super();
+ throw new Error();
+ }
+}
+customElements.define('custom-video', MyCustomVideo, { extends: 'video' });
+</script>
+<video id=targetvideo is='custom-video'> <source></source> </video>
+<script>
+test(t => {
+ let target = document.getElementById('targetvideo');
+ assert_true(!!target);
+ assert_equals(target.localName, 'video');
+ assert_true(target instanceof MyCustomVideo);
+ assert_true(target instanceof HTMLVideoElement);
+ assert_equals(target.children.length, 1);
+}, 'Throwing exception in customized built-in constructor should not crash and should return correct element type (video)');
+</script>
+
+
+<script>
+class MyCustomForm extends HTMLFormElement {
+ constructor() {
+ super();
+ throw new Error();
+ }
+}
+customElements.define('custom-form', MyCustomForm, { extends: 'form' });
+</script>
+<form id=targetform is='custom-form'> <label></label><input> </form>
+<script>
+test(t => {
+ let target = document.getElementById('targetform');
+ assert_true(!!target);
+ assert_equals(target.localName, 'form');
+ assert_true(target instanceof MyCustomForm);
+ assert_true(target instanceof HTMLFormElement);
+ assert_equals(target.children.length, 2);
+}, 'Throwing exception in customized built-in constructor should not crash and should return correct element type (form)');
+</script>
+
+<script>
+class MyInput extends HTMLInputElement { };
+customElements.define('my-input', MyInput, { extends: 'input' });
+</script>
+<input id=customized-input is='my-input'>
+<script>
+test(t => {
+ const input = document.getElementById('customized-input');
+ assert_true(input instanceof MyInput);
+ assert_true(input instanceof HTMLInputElement);
+}, 'Make sure customized <input> element doesnt crash');
+</script>
+
+
+<script>
+class MyInputAttrs extends HTMLInputElement {
+ constructor() {
+ super();
+ this.setAttribute('foo', 'bar');
+ }
+}
+customElements.define('my-input-attr', MyInputAttrs, { extends: 'input' });
+</script>
+<input id=customized-input-attr is='my-input-attr'>
+<script>
+test(t => {
+ const input = document.getElementById('customized-input-attr');
+ assert_true(input instanceof MyInputAttrs);
+ assert_true(input instanceof HTMLInputElement);
+ assert_equals(input.getAttribute('foo'),'bar');
+}, 'Make sure customized <input> element that sets attributes doesnt crash');
+</script>
diff --git a/testing/web-platform/tests/custom-elements/disconnected-callbacks.html b/testing/web-platform/tests/custom-elements/disconnected-callbacks.html
new file mode 100644
index 0000000000..7f5a4d0f8e
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/disconnected-callbacks.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: disconnectedCallback</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="disconnectedCallback must be enqueued whenever custom element is removed from a document">
+<link rel="help" href="https://w3c.github.io/webcomponents/spec/custom/#dfn-connected-callback">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/custom-elements-helpers.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+var calls = [];
+class MyCustomElement extends HTMLElement {
+ connectedCallback() { calls.push('connected', this); }
+ disconnectedCallback() { calls.push('disconnected', this); }
+}
+customElements.define('my-custom-element', MyCustomElement);
+
+document_types().forEach(function (entry) {
+ var documentName = entry.name;
+ var getDocument = entry.create;
+
+ promise_test(function () {
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ doc.documentElement.appendChild(instance);
+ calls = [];
+ doc.documentElement.removeChild(instance);
+ assert_array_equals(calls, ['disconnected', instance]);
+ });
+ }, 'Removing a custom element from ' + documentName + ' must enqueue and invoke disconnectedCallback');
+
+ promise_test(function () {
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ var parent = document.createElement('div');
+ parent.appendChild(instance);
+ doc.documentElement.appendChild(parent);
+ calls = [];
+ doc.documentElement.removeChild(parent);
+ assert_array_equals(calls, ['disconnected', instance]);
+ });
+ }, 'Removing an ancestor of custom element from ' + documentName + ' must enqueue and invoke disconnectedCallback');
+
+ promise_test(function () {
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ var host = doc.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ var shadowRoot = host.attachShadow({mode: 'closed'});
+ doc.documentElement.appendChild(host);
+ shadowRoot.appendChild(instance);
+
+ calls = [];
+ shadowRoot.removeChild(instance);
+ assert_array_equals(calls, ['disconnected', instance]);
+ });
+ }, 'Removing a custom element from a shadow tree in ' + documentName + ' must enqueue and invoke disconnectedCallback');
+
+ promise_test(function () {
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ var host = doc.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ var shadowRoot = host.attachShadow({mode: 'closed'});
+ shadowRoot.appendChild(instance);
+ doc.documentElement.appendChild(host);
+
+ calls = [];
+ doc.documentElement.removeChild(host);
+ assert_array_equals(calls, ['disconnected', instance]);
+ });
+ }, 'Removing the shadow host of a custom element from a' + documentName + ' must enqueue and invoke disconnectedCallback');
+
+ promise_test(function () {
+ return getDocument().then(function (doc) {
+ var instance = document.createElement('my-custom-element');
+ var host = doc.createElementNS('http://www.w3.org/1999/xhtml', 'div');
+ var shadowRoot = host.attachShadow({mode: 'closed'});
+ shadowRoot.appendChild(instance);
+
+ calls = [];
+ shadowRoot.removeChild(instance);
+ assert_array_equals(calls, []);
+ });
+ }, 'Removing a custom element from a detached shadow tree that belongs to ' + documentName + ' must not enqueue and invoke disconnectedCallback');
+});
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/element-internals-shadowroot.html b/testing/web-platform/tests/custom-elements/element-internals-shadowroot.html
new file mode 100644
index 0000000000..7ec0896244
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/element-internals-shadowroot.html
@@ -0,0 +1,130 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>ElementInternals.shadowRoot</title>
+<link rel="author" href="mailto:masonf@chromium.org">
+<link rel="help" href="https://html.spec.whatwg.org/#dom-elementinternals-shadowroot">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+
+test(() => {
+ let constructed = false;
+ customElements.define('custom-open', class extends HTMLElement {
+ constructor() {
+ super();
+ const elementInternals = this.attachInternals();
+ assert_equals(elementInternals.shadowRoot, null);
+ const shadow = this.attachShadow({mode: 'open'});
+ assert_equals(elementInternals.shadowRoot, shadow);
+ constructed = true;
+ }
+ });
+ const element = document.createElement('custom-open');
+ assert_true(constructed);
+}, 'ElementInternals.shadowRoot allows access to open shadow root');
+
+test(() => {
+ let constructed = false;
+ customElements.define('custom-closed', class extends HTMLElement {
+ constructor() {
+ super();
+ const elementInternals = this.attachInternals();
+ assert_equals(elementInternals.shadowRoot, null);
+ const shadow = this.attachShadow({mode: 'closed'});
+ assert_equals(elementInternals.shadowRoot, shadow);
+ assert_equals(this.shadowRoot, null);
+ constructed = true;
+ }
+ });
+ const element = document.createElement('custom-closed');
+ assert_true(constructed);
+}, 'ElementInternals.shadowRoot allows access to closed shadow root');
+
+test(() => {
+ let constructed = false;
+ const element = document.createElement('x-1');
+ assert_throws_dom('NotSupportedError', () => element.attachInternals(),'attachInternals cannot be called before definition exists');
+ customElements.define('x-1', class extends HTMLElement {
+ constructor() {
+ super();
+ assert_true(!!this.attachInternals());
+ constructed = true;
+ }
+ });
+ assert_false(constructed);
+ assert_throws_dom('NotSupportedError', () => element.attachInternals(),'attachInternals cannot be called before constructor');
+ customElements.upgrade(element);
+ assert_true(constructed);
+ assert_throws_dom('NotSupportedError', () => element.attachInternals(),'attachInternals already called');
+}, 'ElementInternals cannot be called before constructor, upgrade case');
+
+test(() => {
+ let constructed = false;
+ const element = document.createElement('x-2');
+ customElements.define('x-2', class extends HTMLElement {
+ constructor() {
+ super();
+ // Don't attachInternals() here
+ constructed = true;
+ }
+ });
+ assert_throws_dom('NotSupportedError', () => element.attachInternals(),'attachInternals cannot be called before constructor');
+ assert_false(constructed);
+ customElements.upgrade(element);
+ assert_true(constructed);
+ assert_true(!!element.attachInternals(),'After the constructor, ok to call from outside');
+}, 'ElementInternals *can* be called after constructor, upgrade case');
+
+test(() => {
+ let constructed = false;
+ customElements.define('x-3', class extends HTMLElement {
+ constructor() {
+ super();
+ assert_true(!!this.attachInternals());
+ constructed = true;
+ }
+ });
+ const element = document.createElement('x-3');
+ assert_true(constructed);
+ assert_throws_dom('NotSupportedError', () => element.attachInternals(), 'attachInternals already called');
+}, 'ElementInternals cannot be called after constructor calls it, create case');
+
+test(() => {
+ let constructed = false;
+ const element = document.createElement('x-5');
+ customElements.define('x-5', class extends HTMLElement {
+ static disabledFeatures = [ 'internals' ];
+ constructor() {
+ super();
+ assert_throws_dom('NotSupportedError', () => this.attachInternals(), 'attachInternals forbidden by disabledFeatures, constructor');
+ constructed = true;
+ }
+ });
+ assert_false(constructed);
+ assert_throws_dom('NotSupportedError', () => element.attachInternals(), 'attachInternals forbidden by disabledFeatures, pre-upgrade');
+ customElements.upgrade(element);
+ assert_true(constructed);
+ assert_throws_dom('NotSupportedError', () => element.attachInternals(), 'attachInternals forbidden by disabledFeatures, post-upgrade');
+}, 'ElementInternals disabled by disabledFeatures');
+
+test(() => {
+ let constructed = false;
+ const element = document.createElement('x-6');
+ const sr = element.attachShadow({mode: 'closed'});
+ assert_true(sr instanceof ShadowRoot);
+ customElements.define('x-6', class extends HTMLElement {
+ constructor() {
+ super();
+ assert_throws_dom('NotSupportedError', () => this.attachShadow({mode:'open'}), 'attachShadow already called');
+ const elementInternals = this.attachInternals();
+ assert_equals(elementInternals.shadowRoot, null, 'ElementInternals.shadowRoot should not be available for pre-attached shadow');
+ constructed = true;
+ }
+ });
+ assert_false(constructed);
+ customElements.upgrade(element);
+ assert_true(constructed,'Failed to construct - test failed');
+ assert_equals(element.shadowRoot, null, 'shadow root is closed');
+}, 'ElementInternals.shadowRoot doesn\'t reveal pre-attached closed shadowRoot');
+</script>
diff --git a/testing/web-platform/tests/custom-elements/enqueue-custom-element-callback-reactions-inside-another-callback.html b/testing/web-platform/tests/custom-elements/enqueue-custom-element-callback-reactions-inside-another-callback.html
new file mode 100644
index 0000000000..2fd932f29a
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/enqueue-custom-element-callback-reactions-inside-another-callback.html
@@ -0,0 +1,223 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: must enqueue an element on the appropriate element queue after checking callback is null and the attribute name</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="To enqueue a custom element callback reaction, the callback must be checked of being null and whether the attribute name is observed or not">
+<link rel="help" content="https://html.spec.whatwg.org/multipage/custom-elements.html#enqueue-a-custom-element-callback-reaction">
+<link rel="help" content="https://github.com/w3c/webcomponents/issues/760">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/custom-elements-helpers.js"></script>
+</head>
+<body>
+<script>
+
+test_with_window((contentWindow, contentDocument) => {
+ class ParentElement extends contentWindow.HTMLElement {
+ connectedCallback()
+ {
+ logs.push('begin');
+ const child = this.firstChild;
+ child.remove();
+ logs.push('end');
+ }
+ }
+ contentWindow.customElements.define('parent-element', ParentElement);
+
+ const logs = [];
+ class ChildElement extends contentWindow.HTMLElement {
+ connectedCallback() { logs.push('connected'); }
+ disconnectedCallback() { logs.push('disconnected'); }
+ }
+ contentWindow.customElements.define('child-element', ChildElement);
+
+ const parent = new ParentElement;
+ const child = new ChildElement;
+ parent.appendChild(child);
+
+ contentDocument.body.appendChild(parent);
+ assert_array_equals(logs, ['begin', 'connected', 'disconnected', 'end']);
+}, 'Disconnecting an element with disconnectedCallback while it has a connectedCallback in its custom element reaction queue must result in connectedCallback getting invoked before the removal completes');
+
+test_with_window((contentWindow, contentDocument) => {
+ class ParentElement extends contentWindow.HTMLElement {
+ connectedCallback()
+ {
+ logs.push('begin');
+ const child = this.firstChild;
+ child.remove();
+ logs.push('end');
+ }
+ }
+ contentWindow.customElements.define('parent-element', ParentElement);
+
+ const logs = [];
+ class ChildElement extends contentWindow.HTMLElement {
+ connectedCallback() { logs.push('connected'); }
+ }
+ contentWindow.customElements.define('child-element', ChildElement);
+
+ const parent = new ParentElement;
+ const child = new ChildElement;
+ parent.appendChild(child);
+
+ contentDocument.body.appendChild(parent);
+ assert_array_equals(logs, ['begin', 'end', 'connected']);
+}, 'Disconnecting an element without disconnectedCallback while it has a connectedCallback in its custom element reaction queue must not result in connectedCallback getting invoked before the removal completes');
+
+test_with_window((contentWindow, contentDocument) => {
+ class ParentElement extends contentWindow.HTMLElement {
+ disconnectedCallback()
+ {
+ logs.push('begin');
+ contentDocument.body.appendChild(this.firstChild);
+ logs.push('end');
+ }
+ }
+ contentWindow.customElements.define('parent-element', ParentElement);
+
+ const logs = [];
+ class ChildElement extends contentWindow.HTMLElement {
+ connectedCallback() { logs.push('connected'); }
+ disconnectedCallback() { logs.push('disconnected'); }
+ }
+ contentWindow.customElements.define('child-element', ChildElement);
+
+ const parent = new ParentElement;
+ const child = new ChildElement;
+ parent.appendChild(child);
+ contentDocument.body.appendChild(parent);
+ parent.remove();
+ assert_array_equals(logs, ['connected', 'begin', 'disconnected', 'connected', 'end']);
+}, 'Connecting a element with connectedCallback while it has a disconnectedCallback in its custom element reaction queue must result in disconnectedCallback getting invoked before the insertion completes');
+
+test_with_window((contentWindow, contentDocument) => {
+ class ParentElement extends contentWindow.HTMLElement {
+ disconnectedCallback()
+ {
+ logs.push('begin');
+ contentDocument.body.appendChild(this.firstChild);
+ logs.push('end');
+ }
+ }
+ contentWindow.customElements.define('parent-element', ParentElement);
+
+ const logs = [];
+ class ChildElement extends contentWindow.HTMLElement {
+ disconnectedCallback() { logs.push('disconnected'); }
+ }
+ contentWindow.customElements.define('child-element', ChildElement);
+
+ const parent = new ParentElement;
+ const child = new ChildElement;
+ parent.appendChild(child);
+ contentDocument.body.appendChild(parent);
+ parent.remove();
+ assert_array_equals(logs, ['begin', 'end', 'disconnected']);
+}, 'Connecting an element without connectedCallback while it has a disconnectedCallback in its custom element reaction queue must not result in disconnectedCallback getting invoked before the insertion completes');
+
+test_with_window((contentWindow, contentDocument) => {
+ class ParentElement extends contentWindow.HTMLElement {
+ connectedCallback()
+ {
+ logs.push('begin');
+ document.adoptNode(this.firstChild);
+ logs.push('end');
+ }
+ }
+ contentWindow.customElements.define('parent-element', ParentElement);
+
+ const logs = [];
+ class ChildElement extends contentWindow.HTMLElement {
+ adoptedCallback() { logs.push('adopted'); }
+ connectedCallback() { logs.push('connected'); }
+ }
+ contentWindow.customElements.define('child-element', ChildElement);
+
+ const parent = new ParentElement;
+ const child = new ChildElement;
+ parent.appendChild(child);
+ contentDocument.body.appendChild(parent);
+ assert_array_equals(logs, ['begin', 'connected', 'adopted', 'end']);
+}, 'Adopting an element with adoptingCallback while it has a connectedCallback in its custom element reaction queue must result in connectedCallback getting invoked before the adoption completes');
+
+test_with_window((contentWindow, contentDocument) => {
+ class ParentElement extends contentWindow.HTMLElement {
+ connectedCallback()
+ {
+ logs.push('begin');
+ document.adoptNode(this.firstChild);
+ logs.push('end');
+ }
+ }
+ contentWindow.customElements.define('parent-element', ParentElement);
+
+ const logs = [];
+ class ChildElement extends contentWindow.HTMLElement {
+ connectedCallback() { logs.push('connected'); }
+ }
+ contentWindow.customElements.define('child-element', ChildElement);
+
+ const parent = new ParentElement;
+ const child = new ChildElement;
+ parent.appendChild(child);
+ contentDocument.body.appendChild(parent);
+ assert_array_equals(logs, ['begin', 'end', 'connected']);
+}, 'Adopting an element without adoptingCallback while it has a connectedCallback in its custom element reaction queue must not result in connectedCallback getting invoked before the adoption completes');
+
+test_with_window((contentWindow, contentDocument) => {
+ class ParentElement extends contentWindow.HTMLElement {
+ connectedCallback()
+ {
+ logs.push('begin');
+ this.firstChild.setAttribute('title', 'foo');
+ logs.push('end');
+ }
+ }
+ contentWindow.customElements.define('parent-element', ParentElement);
+
+ const logs = [];
+ class ChildElement extends contentWindow.HTMLElement {
+ attributeChangedCallback() { logs.push('attributeChanged'); }
+ connectedCallback() { logs.push('connected'); }
+ static get observedAttributes() { return ['title']; }
+ }
+ contentWindow.customElements.define('child-element', ChildElement);
+
+ const parent = new ParentElement;
+ const child = new ChildElement;
+ parent.appendChild(child);
+ contentDocument.body.appendChild(parent);
+ assert_array_equals(logs, ['begin', 'connected', 'attributeChanged', 'end']);
+}, 'Setting an observed attribute on an element with attributeChangedCallback while it has a connectedCallback in its custom element reaction queue must result in connectedCallback getting invoked before the attribute change completes');
+
+test_with_window((contentWindow, contentDocument) => {
+ class ParentElement extends contentWindow.HTMLElement {
+ connectedCallback()
+ {
+ logs.push('begin');
+ this.firstChild.setAttribute('lang', 'en');
+ logs.push('end');
+ }
+ }
+ contentWindow.customElements.define('parent-element', ParentElement);
+
+ const logs = [];
+ class ChildElement extends contentWindow.HTMLElement {
+ attributeChangedCallback() { logs.push('attributeChanged'); }
+ connectedCallback() { logs.push('connected'); }
+ static get observedAttributes() { return ['title']; }
+ }
+ contentWindow.customElements.define('child-element', ChildElement);
+
+ const parent = new ParentElement;
+ const child = new ChildElement;
+ parent.appendChild(child);
+ contentDocument.body.appendChild(parent);
+ assert_array_equals(logs, ['begin', 'end', 'connected']);
+}, 'Setting an observed attribute on an element with attributeChangedCallback while it has a connectedCallback in its custom element reaction queue must not result in connectedCallback getting invoked before the attribute change completes');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-NotSupportedError.html b/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-NotSupportedError.html
new file mode 100644
index 0000000000..51be7183c1
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-NotSupportedError.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(() => {
+ class NotFormAssociatedElement extends HTMLElement {}
+ customElements.define('my-element1', NotFormAssociatedElement);
+ const element = new NotFormAssociatedElement();
+ const i = element.attachInternals();
+
+ assert_throws_dom('NotSupportedError', () => i.setFormValue(''));
+ assert_throws_dom('NotSupportedError', () => i.form);
+ assert_throws_dom('NotSupportedError', () => i.setValidity({}));
+ assert_throws_dom('NotSupportedError', () => i.willValidate);
+ assert_throws_dom('NotSupportedError', () => i.validity);
+ assert_throws_dom('NotSupportedError', () => i.validationMessage);
+ assert_throws_dom('NotSupportedError', () => i.checkValidity());
+ assert_throws_dom('NotSupportedError', () => i.reportValidity());
+ assert_throws_dom('NotSupportedError', () => i.labels);
+}, 'Form-related operations and attributes should throw NotSupportedErrors' +
+ ' for non-form-associated custom elements.');
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-form.html b/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-form.html
new file mode 100644
index 0000000000..fd9363b450
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-form.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<title>form attribute of ElementInternals</title>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+customElements.define('custom-input-parser', class extends HTMLElement {
+ static get formAssociated() { return true; }
+
+ constructor() {
+ super();
+ this.internals_ = this.attachInternals();
+ }
+ get i() { return this.internals_; }
+});
+</script>
+
+<form id="custom-form">
+ <custom-input-parser id="custom-1"></custom-input-parser>
+ <custom-input-upgrade id="custom-2"></custom-input-upgrade>
+</form>
+<custom-input-parser id="custom-3" form="custom-form"></custom-input-parser>
+<custom-input-upgrade id="custom-4" form="custom-form"></custom-input-upgrade>
+<custom-input-upgrade id="custom-5"></custom-input-upgrade>
+
+<script>
+test(() => {
+ const form = document.forms[0];
+ assert_equals(document.getElementById("custom-1").i.form, form);
+ assert_equals(document.getElementById("custom-3").i.form, form);
+
+ // Test upgrading.
+ customElements.define('custom-input-upgrade', class extends HTMLElement {
+ static get formAssociated() { return true; }
+ constructor() {
+ super();
+ this.internals_ = this.attachInternals();
+ }
+ get i() { return this.internals_; }
+ });
+ assert_equals(document.getElementById("custom-2").i.form, form);
+ assert_equals(document.getElementById("custom-4").i.form, form);
+
+ // Test changing name attribuate.
+ let custom5 = document.getElementById("custom-5");
+ assert_equals(custom5.i.form, null);
+ custom5.setAttribute("form", "custom-form");
+ assert_equals(custom5.i.form, form);
+}, "ElementInternals.form should return the target element's form owner");
+
+test(() => {
+ class NotFormAssociatedElement extends HTMLElement {}
+ customElements.define('not-form-associated-element', NotFormAssociatedElement);
+ const element = new NotFormAssociatedElement();
+ const i = element.attachInternals();
+ assert_throws_dom('NotSupportedError', () => i.form);
+}, "ElementInternals.form should throws NotSupportedError if the target element is not a form-associated custom element");
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-labels.html b/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-labels.html
new file mode 100644
index 0000000000..b27be5f2fc
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-labels.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<title>labels attribute of ElementInternals, and label association</title>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="container"></div>
+<script>
+class MyControl extends HTMLElement {
+ static get formAssociated() { return true; }
+
+ constructor() {
+ super();
+ this.internals_ = this.attachInternals();
+ }
+ get i() { return this.internals_; }
+}
+customElements.define('my-control', MyControl);
+const container = document.querySelector('#container');
+
+test(() => {
+ container.innerHTML = '<label><span><my-control></my-control></span></label>';
+ let label = container.querySelector('label');
+ let control = container.querySelector('my-control');
+ assert_equals(label.control, control);
+ assert_true(control.i.labels instanceof NodeList);
+ assert_array_equals(control.i.labels, [label]);
+
+ container.innerHTML = '<label for="mc"></label><form><my-control id="mc"></my-control></form>';
+ label = container.querySelector('label');
+ control = container.querySelector('my-control');
+ assert_equals(label.control, control);
+ assert_equals(label.form, control.i.form);
+ assert_array_equals(control.i.labels, [label]);
+
+ container.innerHTML = '<label for="mc"></label><label for="mc"><my-control id="mc">';
+ const labels = container.querySelectorAll('label');
+ control = container.querySelector('my-control');
+ assert_array_equals(control.i.labels, labels);
+
+ container.innerHTML = '<my-control></my-control>';
+ control = container.querySelector('my-control');
+ assert_array_equals(control.i.labels, []);
+
+ container.innerHTML = '<label><x-foo></x-foo></label>';
+ label = container.querySelector('label');
+ assert_equals(label.control, null);
+}, 'LABEL association');
+
+test(() => {
+ container.innerHTML = '<label for="mc"></label><form><my-control id="mc"></my-control></form>';
+ const control = container.querySelector('my-control');
+ let clickCount = 0;
+ control.addEventListener('click', e => { ++clickCount; });
+ container.querySelector('label').click();
+ assert_equals(clickCount, 1);
+}, 'LABEL click');
+
+test(() => {
+ class NotFormAssociatedElement extends HTMLElement {}
+ customElements.define('not-form-associated-element', NotFormAssociatedElement);
+ const element = new NotFormAssociatedElement();
+ const i = element.attachInternals();
+ assert_throws_dom('NotSupportedError', () => i.labels);
+}, "ElementInternals.labels should throw NotSupportedError if the target element is not a form-associated custom element");
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-reportValidity-bubble-ref.html b/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-reportValidity-bubble-ref.html
new file mode 100644
index 0000000000..54a438245c
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-reportValidity-bubble-ref.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<title>Both focusable and unfocusable custom elements can show validation bubbles</title>
+<link rel=help href=https://html.spec.whatwg.org/C/#report-validity-steps>
+<focusable-custom-element tabindex="0"></focusable-custom-element>
+<script>
+class FocusableCustomElement extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({mode: 'open'});
+ this.shadowRoot.innerHTML = '<input>';
+ this.elementInternals = this.attachInternals();
+ const validationAnchor = this.shadowRoot.querySelector('input');
+ this.elementInternals.setValidity({valueMissing: true}, 'value missing', validationAnchor);
+ }
+
+ static get formAssociated() {
+ return true;
+ }
+
+ reportValidity() {
+ this.elementInternals.reportValidity();
+ }
+}
+
+customElements.define('focusable-custom-element', FocusableCustomElement);
+
+document.querySelector('focusable-custom-element').reportValidity();
+</script>
diff --git a/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-reportValidity-bubble.html b/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-reportValidity-bubble.html
new file mode 100644
index 0000000000..6ada4b473a
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-reportValidity-bubble.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<title>Both focusable and unfocusable custom elements can show validation bubbles</title>
+<link rel=match href=ElementInternals-reportValidity-bubble-ref.html>
+<link rel=help href=https://html.spec.whatwg.org/C/#report-validity-steps>
+<unfocusable-custom-element></unfocusable-custom-element>
+<script>
+class UnfocusableCustomElement extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({mode: 'open'});
+ this.shadowRoot.innerHTML = '<input>';
+ this.elementInternals = this.attachInternals();
+ const validationAnchor = this.shadowRoot.querySelector('input');
+ this.elementInternals.setValidity({valueMissing: true}, 'value missing', validationAnchor);
+ }
+
+ static get formAssociated() {
+ return true;
+ }
+
+ reportValidity() {
+ this.elementInternals.reportValidity();
+ }
+}
+
+customElements.define('unfocusable-custom-element', UnfocusableCustomElement);
+
+document.querySelector('unfocusable-custom-element').reportValidity();
+</script>
diff --git a/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-setFormValue.html b/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-setFormValue.html
new file mode 100644
index 0000000000..8a13973f08
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-setFormValue.html
@@ -0,0 +1,587 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="container"></div>
+<script>
+class MyControl extends HTMLElement {
+ static get formAssociated() { return true; }
+
+ constructor() {
+ super();
+ this.internals_ = this.attachInternals();
+ this.value_ = '';
+ }
+
+ get value() {
+ return this.value_;
+ }
+ set value(v) {
+ this.internals_.setFormValue(v);
+ this.value_ = v;
+ }
+ setValues(nameValues) {
+ const formData = new FormData();
+ for (let p of nameValues) {
+ formData.append(p[0], p[1]);
+ }
+ this.internals_.setFormValue(formData);
+ }
+}
+customElements.define('my-control', MyControl);
+const $ = document.querySelector.bind(document);
+
+function submitPromise(t, extractFromIframe) {
+ if (!extractFromIframe) {
+ extractFromIframe = (iframe) => iframe.contentWindow.location.search;
+ }
+ return new Promise((resolve, reject) => {
+ const iframe = $('iframe');
+ iframe.onload = () => resolve(extractFromIframe(iframe));
+ iframe.onerror = () => reject(new Error('iframe onerror fired'));
+ $('form').submit();
+ });
+}
+
+function testSerializedEntry({name, value, expected, description}) {
+ // urlencoded
+ {
+ const {name: expectedName, value: expectedValue} = expected.urlencoded;
+ promise_test(async t => {
+ $('#container').innerHTML = '<form action="/common/blank.html" target="if1">' +
+ '<my-control></my-control>' +
+ '</form>' +
+ '<iframe name="if1"></iframe>';
+ if (name !== undefined) {
+ $('my-control').setAttribute("name", name);
+ }
+ if (Array.isArray(value)) {
+ $('my-control').setValues(value);
+ } else {
+ $('my-control').value = value;
+ }
+ const query = await submitPromise(t);
+ assert_equals(query, `?${expectedName}=${expectedValue}`);
+ }, `${description} (urlencoded)`);
+ }
+
+ // formdata
+ {
+ const {name: expectedName, filename: expectedFilename, value: expectedValue} = expected.formdata;
+ promise_test(async t => {
+ $('#container').innerHTML =
+ '<form action="/FileAPI/file/resources/echo-content-escaped.py" method="post" enctype="multipart/form-data" target="if1">' +
+ '<my-control></my-control>' +
+ '</form>' +
+ '<iframe name="if1"></iframe>';
+ if (name !== undefined) {
+ $('my-control').setAttribute("name", name);
+ }
+ if (Array.isArray(value)) {
+ $('my-control').setValues(value);
+ } else {
+ $('my-control').value = value;
+ }
+ const escaped = await submitPromise(t, iframe => iframe.contentDocument.body.textContent);
+ const formdata = escaped
+ .replace(/\r\n?|\n/g, "\r\n")
+ .replace(
+ /\\x[0-9A-Fa-f]{2}/g,
+ escape => String.fromCodePoint(parseInt(escape.substring(2), 16))
+ );
+ const boundary = formdata.split("\r\n")[0];
+ const expected = [
+ boundary,
+ ...(() => {
+ if (expectedFilename === undefined) {
+ return [`Content-Disposition: form-data; name="${expectedName}"`];
+ } else {
+ return [
+ `Content-Disposition: form-data; name="${expectedName}"; filename="${expectedFilename}"`,
+ "Content-Type: text/plain"
+ ];
+ }
+ })(),
+ "",
+ expectedValue,
+ boundary + "--",
+ ""
+ ].join("\r\n");
+ assert_equals(formdata, expected);
+ }, `${description} (formdata)`);
+ }
+}
+
+promise_test(t => {
+ $('#container').innerHTML = '<form action="/common/blank.html" target="if1">' +
+ '<input name=name-pd1 value="value-pd1">' +
+ '<my-control></my-control>' +
+ '</form>' +
+ '<iframe name="if1"></iframe>';
+ return submitPromise(t).then(query => {
+ assert_equals(query, '?name-pd1=value-pd1');
+ });
+}, 'Single value - name is missing');
+
+promise_test(t => {
+ $('#container').innerHTML = '<form action="/common/blank.html" target="if1">' +
+ '<input name=name-pd1 value="value-pd1">' +
+ '<my-control name=""></my-control>' +
+ '<input name=name-pd2 value="value-pd2">' +
+ '</form>' +
+ '<iframe name="if1"></iframe>';
+ $('my-control').value = 'value-ce1';
+ return submitPromise(t).then(query => {
+ assert_equals(query, '?name-pd1=value-pd1&name-pd2=value-pd2');
+ });
+}, 'Single value - empty name exists');
+
+promise_test(t => {
+ $('#container').innerHTML = '<form action="/common/blank.html" target="if1" accept-charset=utf-8>' +
+ '<input name=name-pd1 value="value-pd1">' +
+ '<my-control name="name-ce1"></my-control>' +
+ '<my-control name="name-usv"></my-control>' +
+ '<my-control name="name-file"></my-control>' +
+ '</form>' +
+ '<iframe name="if1"></iframe>';
+ const USV_INPUT = 'abc\uDC00\uD800def';
+ const USV_OUTPUT = 'abc\uFFFD\uFFFDdef';
+ const FILE_NAME = 'test_file.txt';
+ $('[name=name-usv]').value = USV_INPUT;
+ $('[name=name-file]').value = new File(['file content'], FILE_NAME);
+ return submitPromise(t).then(query => {
+ assert_equals(query, `?name-pd1=value-pd1&name-usv=${encodeURIComponent(USV_OUTPUT)}&name-file=${FILE_NAME}`);
+ });
+}, 'Single value - Non-empty name exists');
+
+promise_test(t => {
+ $('#container').innerHTML = '<form action="/common/blank.html" target="if1">' +
+ '<input name=name-pd1 value="value-pd1">' +
+ '<my-control name="name-ce1"></my-control>' +
+ '<my-control name="name-ce2"></my-control>' +
+ '</form>' +
+ '<iframe name="if1"></iframe>';
+ $('my-control').value = null;
+ return submitPromise(t).then(query => {
+ assert_equals(query, '?name-pd1=value-pd1');
+ });
+}, 'Null value should submit nothing');
+
+promise_test(t => {
+ $('#container').innerHTML = '<form action="/common/blank.html" target="if1">' +
+ '<input name=name-pd1 value="value-pd1">' +
+ '<my-control name=name-ce1></my-control>' +
+ '</form>' +
+ '<iframe name="if1"></iframe>';
+ $('my-control').value = 'value-ce1';
+ $('my-control').setValues([]);
+ $('my-control').setValues([['sub1', 'subvalue1'],
+ ['sub2', 'subvalue2'],
+ ['sub2', 'subvalue3']]);
+ return submitPromise(t).then(query => {
+ assert_equals(query, '?name-pd1=value-pd1&sub1=subvalue1&sub2=subvalue2&sub2=subvalue3');
+ });
+}, 'Multiple values - name content attribute is ignored');
+
+promise_test(t => {
+ $('#container').innerHTML = '<form action="/common/blank.html" target="if1">' +
+ '<input name=name-pd1 value="value-pd1">' +
+ '<my-control name=name-ce1></my-control>' +
+ '</form>' +
+ '<iframe name="if1"></iframe>';
+ $('my-control').value = 'value-ce1';
+ $('my-control').setValues([]);
+ return submitPromise(t).then(query => {
+ assert_equals(query, '?name-pd1=value-pd1');
+ });
+}, 'setFormValue with an empty FormData should submit nothing');
+
+testSerializedEntry({
+ name: 'a\nb',
+ value: 'c',
+ expected: {
+ urlencoded: {
+ name: 'a%0D%0Ab',
+ value: 'c'
+ },
+ formdata: {
+ name: 'a%0D%0Ab',
+ value: 'c'
+ }
+ },
+ description: 'Newline normalization - \\n in name'
+});
+
+testSerializedEntry({
+ name: 'a\rb',
+ value: 'c',
+ expected: {
+ urlencoded: {
+ name: 'a%0D%0Ab',
+ value: 'c'
+ },
+ formdata: {
+ name: 'a%0D%0Ab',
+ value: 'c'
+ }
+ },
+ description: 'Newline normalization - \\r in name'
+});
+
+testSerializedEntry({
+ name: 'a\r\nb',
+ value: 'c',
+ expected: {
+ urlencoded: {
+ name: 'a%0D%0Ab',
+ value: 'c'
+ },
+ formdata: {
+ name: 'a%0D%0Ab',
+ value: 'c'
+ }
+ },
+ description: 'Newline normalization - \\r\\n in name'
+});
+
+testSerializedEntry({
+ name: 'a\n\rb',
+ value: 'c',
+ expected: {
+ urlencoded: {
+ name: 'a%0D%0A%0D%0Ab',
+ value: 'c'
+ },
+ formdata: {
+ name: 'a%0D%0A%0D%0Ab',
+ value: 'c'
+ }
+ },
+ description: 'Newline normalization - \\n\\r in name'
+});
+
+testSerializedEntry({
+ name: 'a',
+ value: 'b\nc',
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ value: 'b\r\nc'
+ }
+ },
+ description: 'Newline normalization - \\n in value'
+});
+
+testSerializedEntry({
+ name: 'a',
+ value: 'b\rc',
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ value: 'b\r\nc'
+ }
+ },
+ description: 'Newline normalization - \\r in value'
+});
+
+testSerializedEntry({
+ name: 'a',
+ value: 'b\r\nc',
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ value: 'b\r\nc'
+ }
+ },
+ description: 'Newline normalization - \\r\\n in value'
+});
+
+testSerializedEntry({
+ name: 'a',
+ value: 'b\n\rc',
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0A%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ value: 'b\r\n\r\nc'
+ }
+ },
+ description: 'Newline normalization - \\n\\r in value'
+});
+
+testSerializedEntry({
+ name: 'a',
+ value: new File([], "b\nc", {type: "text/plain"}),
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ filename: 'b%0Ac',
+ value: ''
+ }
+ },
+ description: 'Newline normalization - \\n in filename'
+});
+
+testSerializedEntry({
+ name: 'a',
+ value: new File([], "b\rc", {type: "text/plain"}),
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ filename: 'b%0Dc',
+ value: ''
+ }
+ },
+ description: 'Newline normalization - \\r in filename'
+});
+
+testSerializedEntry({
+ name: 'a',
+ value: new File([], "b\r\nc", {type: "text/plain"}),
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ filename: 'b%0D%0Ac',
+ value: ''
+ }
+ },
+ description: 'Newline normalization - \\r\\n in filename'
+});
+
+testSerializedEntry({
+ name: 'a',
+ value: new File([], "b\n\rc", {type: "text/plain"}),
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0A%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ filename: 'b%0A%0Dc',
+ value: ''
+ }
+ },
+ description: 'Newline normalization - \\n\\r in filename'
+});
+
+testSerializedEntry({
+ value: [['a\nb', 'c']],
+ expected: {
+ urlencoded: {
+ name: 'a%0D%0Ab',
+ value: 'c'
+ },
+ formdata: {
+ name: 'a%0D%0Ab',
+ value: 'c'
+ }
+ },
+ description: 'Newline normalization - \\n in FormData name'
+});
+
+testSerializedEntry({
+ value: [['a\rb', 'c']],
+ expected: {
+ urlencoded: {
+ name: 'a%0D%0Ab',
+ value: 'c'
+ },
+ formdata: {
+ name: 'a%0D%0Ab',
+ value: 'c'
+ }
+ },
+ description: 'Newline normalization - \\r in FormData name'
+});
+
+testSerializedEntry({
+ value: [['a\r\nb', 'c']],
+ expected: {
+ urlencoded: {
+ name: 'a%0D%0Ab',
+ value: 'c'
+ },
+ formdata: {
+ name: 'a%0D%0Ab',
+ value: 'c'
+ }
+ },
+ description: 'Newline normalization - \\r\\n in FormData name'
+});
+
+testSerializedEntry({
+ value: [['a\n\rb', 'c']],
+ expected: {
+ urlencoded: {
+ name: 'a%0D%0A%0D%0Ab',
+ value: 'c'
+ },
+ formdata: {
+ name: 'a%0D%0A%0D%0Ab',
+ value: 'c'
+ }
+ },
+ description: 'Newline normalization - \\n\\r in FormData name'
+});
+
+testSerializedEntry({
+ value: [['a', 'b\nc']],
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ value: 'b\r\nc'
+ }
+ },
+ description: 'Newline normalization - \\n in FormData value'
+});
+
+testSerializedEntry({
+ value: [['a', 'b\rc']],
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ value: 'b\r\nc'
+ }
+ },
+ description: 'Newline normalization - \\r in FormData value'
+});
+
+testSerializedEntry({
+ value: [['a', 'b\r\nc']],
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ value: 'b\r\nc'
+ }
+ },
+ description: 'Newline normalization - \\r\\n in FormData value'
+});
+
+testSerializedEntry({
+ value: [['a', 'b\n\rc']],
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0A%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ value: 'b\r\n\r\nc'
+ }
+ },
+ description: 'Newline normalization - \\n\\r in FormData value'
+});
+
+testSerializedEntry({
+ value: [['a', new File([], 'b\nc', {type: "text/plain"})]],
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ filename: 'b%0Ac',
+ value: ''
+ }
+ },
+ description: 'Newline normalization - \\n in FormData filename'
+});
+
+testSerializedEntry({
+ value: [['a', new File([], 'b\rc', {type: "text/plain"})]],
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ filename: 'b%0Dc',
+ value: ''
+ }
+ },
+ description: 'Newline normalization - \\r in FormData filename'
+});
+
+testSerializedEntry({
+ value: [['a', new File([], 'b\r\nc', {type: "text/plain"})]],
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ filename: 'b%0D%0Ac',
+ value: ''
+ }
+ },
+ description: 'Newline normalization - \\r\\n in FormData filename'
+});
+
+testSerializedEntry({
+ value: [['a', new File([], 'b\n\rc', {type: "text/plain"})]],
+ expected: {
+ urlencoded: {
+ name: 'a',
+ value: 'b%0D%0A%0D%0Ac'
+ },
+ formdata: {
+ name: 'a',
+ filename: 'b%0A%0Dc',
+ value: ''
+ }
+ },
+ description: 'Newline normalization - \\n\\r in FormData filename'
+});
+
+test(() => {
+ class NotFormAssociatedElement extends HTMLElement {}
+ customElements.define('not-form-associated-element', NotFormAssociatedElement);
+ const element = new NotFormAssociatedElement();
+ const i = element.attachInternals();
+ assert_throws_dom('NotSupportedError', () => i.setFormValue("test"));
+}, "ElementInternals.setFormValue() should throw NotSupportedError if the target element is not a form-associated custom element");
+
+</script>
diff --git a/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-target-element-is-held-strongly.html b/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-target-element-is-held-strongly.html
new file mode 100644
index 0000000000..a747c04c5c
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-target-element-is-held-strongly.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<title>Target element of ElementsInternals is held strongly and doesn't get GCed if there are no other references</title>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/gc.js"></script>
+
+<script>
+customElements.define("x-foo", class extends HTMLElement {});
+
+promise_test(async t => {
+ const elementInternals = [];
+
+ for (let i = 0; i < 1e5; i++) {
+ const targetElement = document.createElement("x-foo");
+ targetElement.attachShadow({ mode: "open" });
+ elementInternals.push(targetElement.attachInternals());
+ }
+
+ await garbageCollect();
+ await new Promise(r => t.step_timeout(r, 100));
+
+ const allShadowRootsAreAlive = elementInternals.every(eI => eI.shadowRoot instanceof ShadowRoot);
+ assert_true(allShadowRootsAreAlive);
+});
+</script>
diff --git a/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-validation.html b/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-validation.html
new file mode 100644
index 0000000000..8114a5dbd8
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-validation.html
@@ -0,0 +1,356 @@
+<!DOCTYPE html>
+<title>Form validation features of ElementInternals, and :valid :invalid pseudo classes</title>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="container"></div>
+<script>
+class MyControl extends HTMLElement {
+ static get formAssociated() { return true; }
+
+ constructor() {
+ super();
+ this.internals_ = this.attachInternals();
+ }
+ get i() { return this.internals_; }
+}
+customElements.define('my-control', MyControl);
+
+class NotFormAssociatedElement extends HTMLElement {
+ constructor() {
+ super();
+ this.internals_ = this.attachInternals();
+ }
+ get i() { return this.internals_; }
+}
+customElements.define('not-form-associated-element', NotFormAssociatedElement);
+
+test(() => {
+ const control = new MyControl();
+ assert_true(control.i.willValidate, 'default value is true');
+
+ const datalist = document.createElement('datalist');
+ datalist.appendChild(control);
+ assert_false(control.i.willValidate, 'false in DATALIST');
+ datalist.removeChild(control);
+ assert_true(control.i.willValidate, 'remove from DATALIST');
+
+ const fieldset = document.createElement('fieldset');
+ fieldset.disabled = true;
+ fieldset.appendChild(control);
+ assert_false(control.i.willValidate, 'append to disabled FIELDSET');
+ fieldset.disabled = false;
+ assert_true(control.i.willValidate, 'FIELDSET becomes enabled');
+ fieldset.disabled = true;
+ assert_false(control.i.willValidate, 'FIELDSET becomes disabled');
+ fieldset.removeChild(control);
+ assert_true(control.i.willValidate, 'remove from disabled FIELDSET');
+
+ control.setAttribute('disabled', '');
+ assert_false(control.i.willValidate, 'with disabled attribute');
+ control.removeAttribute('disabled');
+ assert_true(control.i.willValidate, 'without disabled attribute');
+
+ control.setAttribute('readonly', '');
+ assert_false(control.i.willValidate, 'with readonly attribute');
+ control.removeAttribute('readonly');
+ assert_true(control.i.willValidate, 'without readonly attribute');
+}, 'willValidate');
+
+test(() => {
+ const container = document.getElementById("container");
+ container.innerHTML = '<will-be-defined></will-be-defined>' +
+ '<will-be-defined disabled></will-be-defined>' +
+ '<will-be-defined readonly></will-be-defined>' +
+ '<datalist><will-be-defined></will-be-defined></datalist>' +
+ '<fieldset disabled><will-be-defined></will-be-defined></fieldset>';
+
+ class WillBeDefined extends HTMLElement {
+ static get formAssociated() { return true; }
+
+ constructor() {
+ super();
+ this.internals_ = this.attachInternals();
+ }
+ get i() { return this.internals_; }
+ }
+ customElements.define('will-be-defined', WillBeDefined);
+ customElements.upgrade(container);
+
+ const controls = document.querySelectorAll('will-be-defined');
+ assert_true(controls[0].i.willValidate, 'default value');
+ assert_false(controls[1].i.willValidate, 'with disabled attribute');
+ assert_false(controls[2].i.willValidate, 'with readOnly attribute');
+ assert_false(controls[3].i.willValidate, 'in datalist');
+ assert_false(controls[4].i.willValidate, 'in disabled fieldset');
+}, 'willValidate after upgrade');
+
+test(t => {
+ const control = document.createElement('will-be-defined-2');
+
+ customElements.define('will-be-defined-2', class extends HTMLElement {
+ static get formAssociated() { return true; }
+ });
+
+ container.append(control);
+ t.add_cleanup(() => { container.innerHTML = ''; });
+
+ const i = control.attachInternals();
+ assert_true(i.willValidate);
+}, 'willValidate after upgrade (document.createElement)');
+
+test(() => {
+ const element = new NotFormAssociatedElement();
+ assert_throws_dom('NotSupportedError', () => element.i.willValidate);
+}, "willValidate should throw NotSupportedError if the target element is not a form-associated custom element");
+
+test(() => {
+ const element = new NotFormAssociatedElement();
+ assert_throws_dom('NotSupportedError', () => element.i.validity);
+}, "validity should throw NotSupportedError if the target element is not a form-associated custom element");
+
+test(() => {
+ const element = new NotFormAssociatedElement();
+ assert_throws_dom('NotSupportedError', () => element.i.validationMessage);
+}, "validationMessage should throw NotSupportedError if the target element is not a form-associated custom element");
+
+test(() => {
+ const element = new NotFormAssociatedElement();
+ assert_throws_dom('NotSupportedError', () => element.i.setValidity());
+}, "setValidity() should throw NotSupportedError if the target element is not a form-associated custom element");
+
+test(() => {
+ const control = document.createElement('my-control');
+ const validity = control.i.validity;
+ assert_false(validity.valueMissing, 'default valueMissing');
+ assert_false(validity.typeMismatch, 'default typeMismatch');
+ assert_false(validity.patternMismatch, 'default patternMismatch');
+ assert_false(validity.tooLong, 'default tooLong');
+ assert_false(validity.tooShort, 'default tooShort');
+ assert_false(validity.rangeUnderflow, 'default rangeUnderflow');
+ assert_false(validity.rangeOverflow, 'default rangeOverflow');
+ assert_false(validity.stepMismatch, 'default stepMismatch');
+ assert_false(validity.badInput, 'default badInput');
+ assert_false(validity.customError, 'default customError');
+ assert_true(validity.valid, 'default valid');
+
+ control.i.setValidity({valueMissing: true}, 'valueMissing message');
+ assert_true(validity.valueMissing);
+ assert_false(validity.valid);
+ assert_equals(control.i.validationMessage, 'valueMissing message');
+
+ control.i.setValidity({typeMismatch: true}, 'typeMismatch message');
+ assert_true(validity.typeMismatch);
+ assert_false(validity.valueMissing);
+ assert_false(validity.valid);
+ assert_equals(control.i.validationMessage, 'typeMismatch message');
+
+ control.i.setValidity({patternMismatch: true}, 'patternMismatch message');
+ assert_true(validity.patternMismatch);
+ assert_false(validity.valid);
+ assert_equals(control.i.validationMessage, 'patternMismatch message');
+
+ control.i.setValidity({tooLong: true}, 'tooLong message');
+ assert_true(validity.tooLong);
+ assert_false(validity.valid);
+ assert_equals(control.i.validationMessage, 'tooLong message');
+
+ control.i.setValidity({tooShort: true}, 'tooShort message');
+ assert_true(validity.tooShort);
+ assert_false(validity.valid);
+ assert_equals(control.i.validationMessage, 'tooShort message');
+
+ control.i.setValidity({rangeUnderflow: true}, 'rangeUnderflow message');
+ assert_true(validity.rangeUnderflow);
+ assert_false(validity.valid);
+ assert_equals(control.i.validationMessage, 'rangeUnderflow message');
+
+ control.i.setValidity({rangeOverflow: true}, 'rangeOverflow message');
+ assert_true(validity.rangeOverflow);
+ assert_false(validity.valid);
+ assert_equals(control.i.validationMessage, 'rangeOverflow message');
+
+ control.i.setValidity({stepMismatch: true}, 'stepMismatch message');
+ assert_true(validity.stepMismatch);
+ assert_false(validity.valid);
+ assert_equals(control.i.validationMessage, 'stepMismatch message');
+
+ control.i.setValidity({badInput: true}, 'badInput message');
+ assert_true(validity.badInput);
+ assert_false(validity.valid);
+ assert_equals(control.i.validationMessage, 'badInput message');
+
+ control.i.setValidity({customError: true}, 'customError message');
+ assert_true(validity.customError, 'customError should be true.');
+ assert_false(validity.valid);
+ assert_equals(control.i.validationMessage, 'customError message');
+
+ // Set multiple flags
+ control.i.setValidity({badInput: true, customError: true}, 'multiple errors');
+ assert_true(validity.badInput);
+ assert_true(validity.customError);
+ assert_false(validity.valid);
+ assert_equals(control.i.validationMessage, 'multiple errors');
+
+ // Clear flags
+ control.i.setValidity({}, 'unnecessary message');
+ assert_false(validity.badInput);
+ assert_false(validity.customError);
+ assert_true(validity.valid);
+ assert_equals(control.i.validationMessage, '');
+
+ assert_throws_js(TypeError, () => { control.i.setValidity({valueMissing: true}); },
+ 'setValidity() requires the second argument if the first argument contains true');
+}, 'validity and setValidity()');
+
+test(() => {
+ document.body.insertAdjacentHTML('afterbegin', '<my-control><light-child></my-control>');
+ let control = document.body.firstChild;
+ const flags = {valueMissing: true};
+ const m = 'non-empty message';
+
+ assert_throws_dom('NotFoundError', () => {
+ control.i.setValidity(flags, m, document.body);
+ }, 'Not a descendant');
+
+ assert_throws_dom('NotFoundError', () => {
+ control.i.setValidity(flags, m, control);
+ }, 'Self');
+
+ let notHTMLElement = document.createElementNS('some-random-namespace', 'foo');
+ control.appendChild(notHTMLElement);
+ assert_throws_js(TypeError, () => {
+ control.i.setValidity(flags, m, notHTMLElement);
+ }, 'Not a HTMLElement');
+ notHTMLElement.remove();
+
+ // A descendant
+ control.i.setValidity(flags, m, control.querySelector('light-child'));
+
+ // An element in the direct shadow tree
+ let shadow1 = control.attachShadow({mode: 'open'});
+ let shadowChild = document.createElement('div');
+ shadow1.appendChild(shadowChild);
+ control.i.setValidity(flags, m, shadowChild);
+
+ // An element in an nested shadow trees
+ let shadow2 = shadowChild.attachShadow({mode: 'closed'});
+ let nestedShadowChild = document.createElement('span');
+ shadow2.appendChild(nestedShadowChild);
+ control.i.setValidity(flags, m, nestedShadowChild);
+}, '"anchor" argument of setValidity()');
+
+test(() => {
+ const element = new NotFormAssociatedElement();
+ assert_throws_dom('NotSupportedError', () => element.i.checkValidity());
+}, "checkValidity() should throw NotSupportedError if the target element is not a form-associated custom element");
+
+test(() => {
+ const control = document.createElement('my-control');
+ let invalidCount = 0;
+ control.addEventListener('invalid', e => {
+ assert_equals(e.target, control);
+ assert_true(e.cancelable);
+ ++invalidCount;
+ });
+
+ assert_true(control.i.checkValidity(), 'default state');
+ assert_equals(invalidCount, 0);
+
+ control.i.setValidity({customError:true}, 'foo');
+ assert_false(control.i.checkValidity());
+ assert_equals(invalidCount, 1);
+}, 'checkValidity()');
+
+test(() => {
+ const element = new NotFormAssociatedElement();
+ assert_throws_dom('NotSupportedError', () => element.i.reportValidity());
+}, "reportValidity() should throw NotSupportedError if the target element is not a form-associated custom element");
+
+test(() => {
+ const control = document.createElement('my-control');
+ document.body.appendChild(control);
+ control.tabIndex = 0;
+ let invalidCount = 0;
+ control.addEventListener('invalid', e => {
+ assert_equals(e.target, control);
+ assert_true(e.cancelable);
+ ++invalidCount;
+ });
+
+ assert_true(control.i.reportValidity(), 'default state');
+ assert_equals(invalidCount, 0);
+
+ control.i.setValidity({customError:true}, 'foo');
+ assert_false(control.i.reportValidity());
+ assert_equals(invalidCount, 1);
+
+ control.blur();
+ control.addEventListener('invalid', e => e.preventDefault());
+ assert_false(control.i.reportValidity());
+}, 'reportValidity()');
+
+test(() => {
+ const container = document.getElementById('container');
+ container.innerHTML = '<form><input type=submit><my-control>';
+ const form = container.querySelector('form');
+ const control = container.querySelector('my-control');
+ control.tabIndex = 0;
+ let invalidCount = 0;
+ control.addEventListener('invalid', e => { ++invalidCount; });
+
+ assert_true(control.i.checkValidity());
+ assert_true(form.checkValidity());
+ control.i.setValidity({valueMissing: true}, 'Please fill out this field');
+ assert_false(form.checkValidity());
+ assert_equals(invalidCount, 1);
+
+ assert_false(form.reportValidity());
+ assert_equals(invalidCount, 2);
+
+ container.querySelector('input[type=submit]').click();
+ assert_equals(invalidCount, 3);
+}, 'Custom control affects validation at the owner form');
+
+function isValidAccordingToCSS(element, comment) {
+ assert_true(element.matches(':valid'), comment ? (comment + ' - :valid') : undefined);
+ assert_false(element.matches(':invalid'), comment ? (comment + ' - :invalid') : undefined);
+}
+
+function isInvalidAccordingToCSS(element, comment) {
+ assert_false(element.matches(':valid'), comment ? (comment + ' - :valid') : undefined);
+ assert_true(element.matches(':invalid'), comment ? (comment + ' - :invalid') : undefined);
+}
+
+test(() => {
+ const container = document.getElementById('container');
+ container.innerHTML = '<form><fieldset><my-control>';
+ const form = container.querySelector('form');
+ const fieldset = container.querySelector('fieldset');
+ const control = container.querySelector('my-control');
+
+ isValidAccordingToCSS(control);
+ isValidAccordingToCSS(form);
+ isValidAccordingToCSS(fieldset);
+
+ control.i.setValidity({typeMismatch: true}, 'Invalid format');
+ isInvalidAccordingToCSS(control);
+ isInvalidAccordingToCSS(form);
+ isInvalidAccordingToCSS(fieldset);
+
+ control.remove();
+ isInvalidAccordingToCSS(control);
+ isValidAccordingToCSS(form);
+ isValidAccordingToCSS(fieldset);
+
+ fieldset.appendChild(control);
+ isInvalidAccordingToCSS(form);
+ isInvalidAccordingToCSS(fieldset);
+
+ control.i.setValidity({});
+ isValidAccordingToCSS(control);
+ isValidAccordingToCSS(form);
+ isValidAccordingToCSS(fieldset);
+}, 'Custom control affects :valid :invalid for FORM and FIELDSET');
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/form-associated/disabled-delegatesFocus.html b/testing/web-platform/tests/custom-elements/form-associated/disabled-delegatesFocus.html
new file mode 100644
index 0000000000..3f2b765a63
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/form-associated/disabled-delegatesFocus.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<meta charset="utf8">
+<title>delegatesFocus on disabled form-associated custom elements</title>
+<link rel="author" href="mailto:avandolder@mozilla.com">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ class CustomControl extends HTMLElement {
+ static get formAssociated() {return true;}
+
+ constructor() {
+ super();
+ this.internals = this.attachInternals();
+
+ this.attachShadow({mode: "open", delegatesFocus: true});
+ this.shadowRoot.append(
+ document.querySelector("template").content.cloneNode(true)
+ );
+
+ this.eventFired = false;
+ this.addEventListener("focus", () => this.eventFired = true);
+ }
+ }
+
+ customElements.define("custom-control", CustomControl)
+</script>
+
+<template>
+ <div tabindex=0 id="target">
+ <slot></slot>
+ </div>
+</template>
+
+<form>
+ <custom-control disabled>Text</custom-control>
+</form>
+
+<script>
+ let focusinPropagated = false;
+ let focusoutPropagated = false;
+
+ const form = document.querySelector("form");
+ form.addEventListener("focusin", () => focusinPropagated = true);
+ form.addEventListener("focusout", () => focusoutPropagated = true);
+
+ test(() => {
+ const element = document.querySelector("custom-control");
+ element.focus();
+
+ assert_true(element.eventFired, "Focus event fired on custom control");
+ assert_true(focusinPropagated, "FocusIn event propagated through control");
+
+ element.blur();
+ assert_true(focusoutPropagated, "FocusOut event propagated through control");
+ }, "Focus events fire on disabled form-associated custom elements with delegatesFocus");
+</script>
diff --git a/testing/web-platform/tests/custom-elements/form-associated/fieldset-elements.html b/testing/web-platform/tests/custom-elements/form-associated/fieldset-elements.html
new file mode 100644
index 0000000000..dc42a57b70
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/form-associated/fieldset-elements.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+customElements.define('custom-input-parser', class extends HTMLElement {
+ static get formAssociated() {return true;}
+});
+</script>
+
+<form>
+ <fieldset id="fs_outer">
+ <custom-input-parser name="custom-1"></custom-input>
+ <custom-input-upgrade name="custom-2"></custom-input>
+ <fieldset id="fs_inner">
+ <custom-input-parser name="custom-3"></custom-input>
+ <custom-input-upgrade name="custom-4"></custom-input>
+ <uncustom-input></custom-input>
+ </fieldset>
+ </fieldset>
+ <custom-input-parser name="custom-5"></custom-input>
+ <custom-input-upgrade name="custom-6"></custom-input>
+</form>
+
+<script>
+test(() => {
+ const formElements = document.forms[0].elements;
+ const fs_outer = document.getElementById("fs_outer");
+ const fs_inner = document.getElementById("fs_inner");
+
+ assert_array_equals(fs_inner.elements, [formElements['custom-3']],
+ "The items in the collection must be children of the inner fieldset element.");
+ assert_array_equals(fs_outer.elements, [formElements['custom-1'], fs_inner, formElements['custom-3']],
+ "The items in the collection must be descendants of the outer fieldset element.");
+
+ customElements.define('custom-input-upgrade', class extends HTMLElement {
+ static get formAssociated() {return true;}
+ });
+
+ assert_array_equals(fs_inner.elements, [formElements['custom-3'], formElements['custom-4']],
+ "The items in the collection must be children of the inner fieldset element.");
+ assert_array_equals(fs_outer.elements,
+ [formElements['custom-1'], formElements['custom-2'], fs_inner, formElements['custom-3'], formElements['custom-4']],
+ "The items in the collection must be descendants of the outer fieldset element.");
+
+}, 'Form associated custom elements should work with fieldset.elements');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/form-associated/form-associated-callback.html b/testing/web-platform/tests/custom-elements/form-associated/form-associated-callback.html
new file mode 100644
index 0000000000..7feec50fef
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/form-associated/form-associated-callback.html
@@ -0,0 +1,249 @@
+<!DOCTYPE html>
+<title>formAssociatedCallback, and form IDL attribute of ElementInternals</title>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+class PreDefined extends HTMLElement {
+ static get formAssociated() { return true; }
+
+ constructor() {
+ super();
+ this.internals_ = this.attachInternals();
+ this.formHistory_ = [];
+ }
+
+ formAssociatedCallback(nullableForm) {
+ this.formHistory_.push(nullableForm);
+ }
+ formHistory() {
+ return this.formHistory_;
+ }
+
+ get form() {
+ return this.internals_.form;
+ }
+}
+customElements.define('pre-defined', PreDefined);
+</script>
+<div id="container">
+
+<fieldset id="fs1">
+<form id="form1">
+<input>
+<pre-defined id="pd1"></pre-defined>
+<select></select>
+</form>
+</fieldset>
+
+<fieldset id="fs2">
+<pre-defined id="pd2" form="form2"></pre-defined>
+<form id="form2">
+<input>
+<select></select>
+</form>
+</fieldset>
+<pre-defined id="pd3" form="form2"></pre-defined>
+
+<table>
+<fieldset id="fs3">
+<form id="form3">
+<tr><td><select></select></tr>
+<tr><td><pre-defined id="pd4"></pre-defined></tr>
+<tr><td><input></tr>
+</form> <!-- The end tag is bogus. -->
+</fieldset> <!-- The end tag is bogus. -->
+<table>
+
+</div>
+
+<script>
+const $ = document.querySelector.bind(document);
+
+test(() => {
+ let controls = $('#form1').elements;
+ assert_equals(controls.length, 3);
+ assert_equals(controls[1], $('#pd1'), 'form.elements');
+ assert_equals($('#pd1').form, $('#form1'));
+ assert_array_equals($('#pd1').formHistory(), [$('#form1')]);
+ assert_equals($('#fs1').elements[1], $('#pd1'), 'fieldset.elements');
+
+ controls = $('#form2').elements;
+ assert_equals(controls.length, 4);
+ assert_equals(controls[0], $('#pd2'), 'form.elements');
+ assert_equals(controls[3], $('#pd3'));
+ assert_equals($('#pd2').form, $('#form2'));
+ assert_equals($('#pd3').form, $('#form2'));
+ assert_array_equals($('#pd2').formHistory(), [$('#form2')]);
+ assert_array_equals($('#pd3').formHistory(), [$('#form2')]);
+ controls = $('#fs2').elements;
+ assert_equals(controls.length, 3);
+ assert_equals(controls[0], $('#pd2'), 'fieldset.elements');
+
+ controls = $('#form3').elements;
+ assert_equals(controls.length, 2);
+ assert_not_equals(controls[1], $('#pd4'));
+ assert_equals($('#fs3').elements.length, 0);
+}, 'Associate by parser, customized at element creation');
+
+test(() => {
+ $('#container').innerHTML = '<fieldset id="fs1"><form id="form1"><input><will-be-defined id="wbd1">' +
+ '</will-be-defined><select></select></form></fieldset>' +
+ '<fieldset id="fs2"><will-be-defined id="wbd2" form="form2"></will-be-defined>' +
+ '<form id="form2"></form></fieldset><will-be-defined id="wbd3" form="form2"></will-be-defined>';
+ let controls = $('#form1').elements;
+ assert_equals(controls.length, 2);
+ assert_not_equals(controls[1], $('#wbd1'));
+ controls = $('#fs1').elements;
+ assert_equals(controls.length, 2);
+ assert_not_equals(controls[1], $('#wbd1'));
+
+ assert_equals($('#form2').elements.length, 0);
+ assert_equals($('#fs2').elements.length, 0);
+
+ class WillBeDefined extends HTMLElement {
+ static get formAssociated() { return true; }
+
+ constructor() {
+ super();
+ this.internals_ = this.attachInternals();
+ this.formHistory_ = [];
+ }
+
+ formAssociatedCallback(nullableForm) {
+ this.formHistory_.push(nullableForm);
+ }
+ formHistory() {
+ return this.formHistory_;
+ }
+
+ get form() {
+ return this.internals_.form;
+ }
+ }
+ customElements.define('will-be-defined', WillBeDefined);
+ customElements.upgrade(container);
+
+ controls = $('#form1').elements;
+ assert_equals(controls.length, 3, 'form.elements.length');
+ assert_equals(controls[1], $('#wbd1'));
+ assert_equals($('#wbd1').form, $('#form1'));
+ controls = $('#fs1').elements;
+ assert_equals(controls.length, 3, 'fieldset.elements.length');
+ assert_equals(controls[1], $('#wbd1'));
+
+ controls = $('#form2').elements;
+ assert_equals($('#wbd2').form, $('#form2'));
+ assert_equals($('#wbd3').form, $('#form2'));
+ assert_array_equals($('#wbd2').formHistory(), [$('#form2')]);
+ assert_array_equals($('#wbd3').formHistory(), [$('#form2')]);
+ assert_equals(controls.length, 2, 'form.elements.length');
+ assert_equals(controls[0], $('#wbd2'));
+ assert_equals(controls[1], $('#wbd3'));
+ controls = $('#fs2').elements;
+ assert_equals(controls.length, 1, 'fieldset.elements.length');
+ assert_equals(controls[0], $('#wbd2'));
+}, 'Parsed, connected, then upgraded');
+
+test(() => {
+ $('#container').innerHTML = '<fieldset id="fs1"><form id="form1"><input><pre-defined id="pd1">' +
+ '</pre-defined><select></select></form></fieldset>' +
+ '<fieldset id="fs2"><pre-defined id="pd2" form="form2"></pre-defined>' +
+ '<form id="form2"></form></fieldset><pre-defined id="pd3" form="form2"></pre-defined>';
+
+ const pd1 = $('#pd1');
+ assert_equals($('#form1').elements.length, 3, 'form.elements.length before removal');
+ assert_equals($('#fs1').elements.length, 3, 'fildset.elements.length before removal');
+ pd1.remove();
+ assert_equals(pd1.form, null);
+ assert_array_equals(pd1.formHistory(), [$('#form1'), null]);
+ assert_equals($('#form1').elements.length, 2, 'form.elements.length after removal');
+ assert_equals($('#fs1').elements.length, 2, 'fildset.elements.length after removal');
+
+ const pd2 = $('#pd2');
+ const pd3 = $('#pd3');
+ assert_equals($('#form2').elements.length, 2, 'form.elements.length before removal');
+ assert_equals($('#fs2').elements.length, 1, 'fieldset.elements.length before removal');
+ pd2.remove();
+ pd3.remove();
+ assert_equals(pd2.form, null);
+ assert_equals(pd3.form, null);
+ assert_array_equals(pd2.formHistory(), [$('#form2'), null]);
+ assert_array_equals(pd3.formHistory(), [$('#form2'), null]);
+ assert_equals($('#form2').elements.length, 0, 'form.elements.length after removal');
+ assert_equals($('#fs2').elements.length, 0, 'fieldset.elements.length after removal');
+}, 'Disassociation');
+
+test(() => {
+ $('#container').innerHTML = '<form id="form1"></form>' +
+ '<pre-defined id="pd1"></pre-defined><form id="form2">' +
+ '</form><form id="form3"></form>';
+ const pd1 = $('#pd1');
+ const form1 = $('#form1');
+ const form2 = $('#form2');
+ const form3 = $('#form3');
+ assert_equals(pd1.form, null);
+ assert_array_equals(pd1.formHistory(), []);
+
+ pd1.setAttribute('form', 'form1');
+ assert_equals(pd1.form, form1);
+ assert_array_equals(pd1.formHistory(), [form1]);
+
+ pd1.setAttribute('form', 'invalid');
+ assert_equals(pd1.form, null);
+ assert_array_equals(pd1.formHistory(), [form1, null]);
+
+ pd1.setAttribute('form', 'form2');
+ assert_equals(pd1.form, form2);
+ assert_array_equals(pd1.formHistory(), [form1, null, form2]);
+
+ pd1.setAttribute('form', 'form3');
+ assert_equals(pd1.form, form3);
+ assert_array_equals(pd1.formHistory(), [form1, null, form2, form3]);
+
+ $('#container').innerHTML = '';
+ assert_equals(pd1.form, null);
+ assert_array_equals(pd1.formHistory(), [form1, null, form2, form3, null]);
+}, 'Updating "form" content attribute');
+
+test(() => {
+ $('#container').innerHTML = '<form></form>' +
+ '<pre-defined id="pd1" form="target"></pre-defined>' +
+ '<form></form><form></form>';
+ const pd1 = $('#pd1');
+ const [form1, form2, form3] = document.forms;
+ assert_equals(pd1.form, null);
+ assert_array_equals(pd1.formHistory(), []);
+
+ // The form1 is the only form element with id='target'.
+ form1.setAttribute('id', 'target');
+ assert_equals(pd1.form, form1);
+ assert_array_equals(pd1.formHistory(), [form1]);
+
+ // The first form element with id='target' is still form1.
+ form3.setAttribute('id', 'target');
+ assert_equals(pd1.form, form1);
+ assert_array_equals(pd1.formHistory(), [form1]);
+
+ // The form3 is the only form element with id='target'.
+ form1.removeAttribute('id');
+ assert_equals(pd1.form, form3);
+ assert_array_equals(pd1.formHistory(), [form1, form3]);
+
+ // The first form element with id='target' is form2 now.
+ form2.setAttribute('id', 'target');
+ assert_equals(pd1.form, form2);
+ assert_array_equals(pd1.formHistory(), [form1, form3, form2]);
+
+ // The form2 is the only form element with id='target'.
+ form3.removeAttribute('id');
+ assert_equals(pd1.form, form2);
+ assert_array_equals(pd1.formHistory(), [form1, form3, form2]);
+
+ // No form element has id='target'.
+ form2.removeAttribute('id');
+ assert_equals(pd1.form, null);
+ assert_array_equals(pd1.formHistory(), [form1, form3, form2, null]);
+}, 'Updating "id" attribute of form element');
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/form-associated/form-disabled-callback.html b/testing/web-platform/tests/custom-elements/form-associated/form-disabled-callback.html
new file mode 100644
index 0000000000..c61a7719fc
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/form-associated/form-disabled-callback.html
@@ -0,0 +1,136 @@
+<!DOCTYPE html>
+<title>formDisabledCallback, and :disabled :enabled pseudo classes</title>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/html/semantics/forms/form-submission-0/resources/targetted-form.js"></script>
+<script>
+class MyControl extends HTMLElement {
+ static get formAssociated() { return true; }
+
+ constructor() {
+ super();
+ this.internals_ = this.attachInternals();
+ this.internals_.setFormValue('my-control-value');
+ this.disabledHistory_ = [];
+ }
+
+ formDisabledCallback(isDisabled) {
+ this.disabledHistory_.push(isDisabled);
+ }
+ disabledHistory() {
+ return this.disabledHistory_;
+ }
+}
+customElements.define('my-control', MyControl);
+
+test(() => {
+ const control = new MyControl();
+ assert_true(control.matches(':enabled'));
+ assert_false(control.matches(':disabled'));
+
+ control.setAttribute('disabled', '');
+ assert_false(control.matches(':enabled'));
+ assert_true(control.matches(':disabled'));
+
+ control.removeAttribute('disabled', '');
+ assert_true(control.matches(':enabled'));
+ assert_false(control.matches(':disabled'));
+
+ assert_array_equals(control.disabledHistory(), [true, false]);
+}, 'Adding/removing disabled content attribute');
+
+test(() => {
+ const container = document.createElement('fieldset');
+ container.innerHTML = '<fieldset><fieldset><my-control></my-control></fieldset></fieldset>';
+ const middleFieldset = container.firstChild;
+ const control = container.querySelector('my-control');
+
+ assert_true(control.matches(':enabled'));
+ assert_false(control.matches(':disabled'));
+
+ middleFieldset.disabled = true;
+ assert_false(control.matches(':enabled'));
+ assert_true(control.matches(':disabled'));
+
+ middleFieldset.disabled = false;
+ assert_true(control.matches(':enabled'));
+ assert_false(control.matches(':disabled'));
+
+ container.disabled = true;
+ assert_false(control.matches(':enabled'));
+ assert_true(control.matches(':disabled'));
+ control.remove();
+ assert_true(control.matches(':enabled'));
+ assert_false(control.matches(':disabled'));
+
+ middleFieldset.appendChild(control);
+ assert_false(control.matches(':enabled'));
+ assert_true(control.matches(':disabled'));
+
+ assert_array_equals(control.disabledHistory(), [true, false, true, false, true]);
+}, 'Relationship with FIELDSET');
+
+test(() => {
+ const form = document.createElement('form');
+ document.body.appendChild(form);
+ form.innerHTML = '<my-control name="n1" disabled></my-control><input name="n2">'
+ const formData = new FormData(form);
+ assert_equals(formData.get('n1'), null);
+}, 'A disabled form-associated custom element should not provide an entry for it');
+
+promise_test(async t => {
+ let form = populateForm('<my-control name=d disabled></my-control>' +
+ '<my-control name=e></my-control>');
+ let query = await submitPromise(form, form.previousSibling);
+ assert_equals(query.indexOf('d=my-control-value'), -1);
+ assert_not_equals(query.indexOf('e=my-control-value'), -1);
+}, 'A disabled form-associated custom element should not submit an entry for it');
+
+test(() => {
+ const control = new MyControl();
+ document.body.appendChild(control);
+ control.setAttribute('tabindex', '0');
+ control.setAttribute('disabled', '');
+ control.focus();
+ assert_not_equals(document.activeElement, control);
+
+ control.removeAttribute('disabled');
+ control.focus();
+ assert_equals(document.activeElement, control);
+}, 'Disabled attribute affects focus-capability');
+
+test(() => {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ // inneHTML upgrades my-control at its CEReacions timing.
+ container.innerHTML = '<my-control disabled>';
+ assert_array_equals(container.firstChild.disabledHistory(), [true]);
+
+ container.innerHTML = '<fieldset disabled><my-control>';
+ assert_array_equals(container.querySelector('my-control').disabledHistory(), [true]);
+}, 'Upgrading an element with disabled content attribute');
+
+test(() => {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ container.innerHTML = '<fieldset disabled><my-control></my-control></fieldset>';
+
+ const control = container.querySelector('my-control');
+ control.setAttribute('disabled', '');
+ control.removeAttribute('disabled');
+ assert_array_equals(control.disabledHistory(), [true]);
+}, 'Toggling "disabled" attribute on a custom element inside disabled <fieldset> does not trigger a callback');
+
+test(() => {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ container.innerHTML = '<fieldset><my-control disabled></my-control></fieldset>';
+
+ const fieldset = container.firstElementChild;
+ fieldset.disabled = true;
+ fieldset.disabled = false;
+ assert_array_equals(container.querySelector('my-control').disabledHistory(), [true]);
+}, 'Toggling "disabled" attribute on a <fieldset> does not trigger a callback on disabled custom element descendant');
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/form-associated/form-elements-namedItem.html b/testing/web-platform/tests/custom-elements/form-associated/form-elements-namedItem.html
new file mode 100644
index 0000000000..ab39422b5e
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/form-associated/form-elements-namedItem.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+customElements.define('custom-input', class extends HTMLElement {
+ static get formAssociated() {return true;}
+});
+</script>
+
+<form id="custom-form">
+ <custom-input id="custom-1" name="alone"></custom-input>
+ <custom-input id="custom-2" name="group"></custom-input>
+ <custom-input id="custom-3" name="group"></custom-input>
+ <custom-button id="custom-4" name="upgrade"></custom-button>
+</form>
+<custom-input id="custom-5" name="group" form="custom-form"></custom-input>
+<custom-button id="custom-6" name="group" form="custom-form"></custom-button>
+
+<script>
+test(() => {
+ const formElements = document.forms[0].elements;
+ assert_equals(formElements['invalid'],undefined);
+ assert_equals(formElements['alone'],document.getElementById('custom-1'),'Single input should be returned as-is');
+ assert_true(formElements['group'] instanceof RadioNodeList,'Repeated names should result in RadioNodeList');
+ const expected = [document.getElementById('custom-2'),
+ document.getElementById('custom-3'),
+ document.getElementById('custom-5')];
+ assert_array_equals(formElements['group'],expected,'Repeated names should be contained in RadioNodeList, in tree order');
+}, 'Form associated custom elements should work with document.forms.elements.namedItem()');
+
+test(() => {
+ const formElements = document.forms[0].elements;
+ assert_equals(formElements['upgrade'],undefined);
+ customElements.define('custom-button', class extends HTMLElement {
+ static get formAssociated() {return true;}
+ });
+ assert_equals(formElements['upgrade'],document.getElementById('custom-4'),'Single button should be returned after upgrading');
+ const expected = [document.getElementById('custom-2'),
+ document.getElementById('custom-3'),
+ document.getElementById('custom-5'),
+ document.getElementById('custom-6')];
+ assert_array_equals(formElements['group'],expected,'Repeated names should be contained in RadioNodeList, in tree order after upgrading');
+}, 'Form associated custom elements should work with document.forms.elements.namedItem() after upgrading');
+
+test(() => {
+ const formElements = document.forms[0].elements;
+ assert_equals(formElements['alone'],document.getElementById('custom-1'),'Single input should be returned as-is');
+ const expected = [document.getElementById('custom-2'),
+ document.getElementById('custom-3'),
+ document.getElementById('custom-5'),
+ document.getElementById('custom-6')];
+ assert_array_equals(formElements['group'],expected,'Repeated names should be contained in RadioNodeList, in tree order after upgrading');
+ document.getElementById('custom-1').setAttribute("name", "group");
+ assert_equals(formElements['alone'],undefined);
+ const expectedNew = [document.getElementById('custom-1'),
+ document.getElementById('custom-2'),
+ document.getElementById('custom-3'),
+ document.getElementById('custom-5'),
+ document.getElementById('custom-6')];
+ assert_array_equals(formElements['group'],expectedNew,'Repeated names should be contained in RadioNodeList, in tree order after updating name attribute');
+}, 'Form associated custom elements should work with document.forms.elements.namedItem() after updating the name attribute');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/form-associated/form-reset-callback.html b/testing/web-platform/tests/custom-elements/form-associated/form-reset-callback.html
new file mode 100644
index 0000000000..8b8497f8b6
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/form-associated/form-reset-callback.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+class MyControl extends HTMLElement {
+ static get formAssociated() { return true; }
+
+ constructor() {
+ super();
+ this.resetCalled_ = false;
+ }
+
+ formResetCallback() {
+ this.resetCalled_ = true;
+ }
+ get resetCalled() { return this.resetCalled_; }
+}
+customElements.define('my-control', MyControl);
+
+test(() => {
+ document.body.insertAdjacentHTML('beforeend',
+ '<form><my-control></my-control></form>');
+ let form = document.body.lastChild;
+ let custom = form.firstChild;
+ form.reset();
+ assert_true(custom.resetCalled);
+}, 'form.reset() should trigger formResetCallback');
+
+test(() => {
+ document.body.insertAdjacentHTML('beforeend',
+ '<form><my-control></my-control><output>default</output></form>');
+ let form = document.body.lastChild;
+ let custom = form.firstChild;
+ let output = form.lastChild;
+ output.value = 'updated';
+ output.addEventListener('DOMSubtreeModified', () => {
+ assert_false(custom.resetCalled, 'formResetCallback should not be ' +
+ 'called before built-in control\'s reset');
+ });
+ form.reset();
+ assert_true(custom.resetCalled);
+}, 'form.reset(): formResetCallback is called after reset of the last ' +
+ 'built-in form control and before the next statement.');
+
+promise_test(() => {
+ document.body.insertAdjacentHTML('beforeend',
+ '<form><my-control></my-control><input type=reset></form>');
+ let form = document.body.lastChild;
+ let custom = form.firstChild;
+ let resetButton = form.lastChild;
+ assert_false(custom.resetCalled);
+ resetButton.click();
+ assert_false(custom.resetCalled);
+ return Promise.resolve().then(() => assert_true(custom.resetCalled));
+}, 'Clicking a reset button invokes formResetCallback in a microtask');
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/form-associated/label-delegatesFocus.html b/testing/web-platform/tests/custom-elements/form-associated/label-delegatesFocus.html
new file mode 100644
index 0000000000..74d31363c9
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/form-associated/label-delegatesFocus.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<link rel=author href="mailto:jarhar@chromium.org">
+<link rel=help href="https://bugs.chromium.org/p/chromium/issues/detail?id=1300587">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<form>
+ <label for=custom>label</label>
+ <my-custom-element id=custom></my-custom-element>
+</form>
+
+<script>
+class MyCustomElement extends HTMLElement {
+ static formAssociated = true;
+ constructor() {
+ super();
+ const root = this.attachShadow({
+ delegatesFocus: true,
+ mode: 'open'
+ });
+ root.appendChild(document.createElement('input'));
+ }
+};
+customElements.define('my-custom-element', MyCustomElement);
+
+window.onload = () => {
+ promise_test(async () => {
+ const label = document.querySelector('label');
+ const customElement = document.querySelector('my-custom-element');
+ const input = customElement.shadowRoot.querySelector('input');
+ await new Promise((resolve) => {
+ input.addEventListener("focus", resolve, {once: true});
+ test_driver.click(label);
+ });
+ assert_equals(document.activeElement, customElement);
+ assert_equals(customElement.shadowRoot.activeElement, input);
+ }, `Clicking on a label for a form associated custom element with delegatesFocus should focus the custom element's focus delegate.`);
+};
+</script>
diff --git a/testing/web-platform/tests/custom-elements/historical.html b/testing/web-platform/tests/custom-elements/historical.html
new file mode 100644
index 0000000000..5a961b13ad
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/historical.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<title>Custom Elements v0 features</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+test(() => {
+ assert_false('registerElement' in document)
+}, 'document.registerElement should not exist')
+
+// These tests should pass as long as v0 isn't supported:
+// v0: a 2nd string argument for createElement.
+// v1: a ElementCreationOptions (dictionary) argument.
+test(() => {
+ try {
+ const element = document.createElement('x', 'string')
+ // If neither v0/v1 are supported, then there should be no is attribute.
+ assert_false(element.hasAttribute('is'))
+ } catch (e) {
+ // If v1 is supported, then converting string to dictionary should throw.
+ assert_throws_js(TypeError, function() { throw e })
+ }
+}, 'document.createElement(localName, "string") should not work')
+
+// createElementNS is analagous, but for the 3rd argument
+test(() => {
+ try {
+ const element = document.createElementNS(null, 'x', 'string')
+ // If neither v0/v1 are supported, then there should be no is attribute.
+ assert_false(element.hasAttribute('is'))
+ } catch (e) {
+ // If v1 is supported, then converting string to dictionary should throw.
+ assert_throws_js(TypeError, function() { throw e })
+ }
+}, 'document.createElementNS(namespace, qualifiedName, "string") should not work')
+</script>
diff --git a/testing/web-platform/tests/custom-elements/htmlconstructor/newtarget-customized-builtins.html b/testing/web-platform/tests/custom-elements/htmlconstructor/newtarget-customized-builtins.html
new file mode 100644
index 0000000000..60c63936d4
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/htmlconstructor/newtarget-customized-builtins.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<title>Custom Elements: [HTMLConstructor] derives prototype from NewTarget</title>
+<meta name="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+<meta name="help" content="https://html.spec.whatwg.org/multipage/dom.html#htmlconstructor">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<body>
+<script>
+"use strict";
+
+[null, undefined, 5, "string"].forEach(function (notAnObject) {
+ test_with_window(w => {
+ // We have to return an object during define(), but not during super()
+ let returnNotAnObject = false;
+
+ function TestElement() {
+ const o = Reflect.construct(w.HTMLParagraphElement, [], new.target);
+
+ assert_equals(Object.getPrototypeOf(o), window.HTMLParagraphElement.prototype,
+ "Must use the HTMLParagraphElement from the realm of NewTarget");
+ assert_not_equals(Object.getPrototypeOf(o), w.HTMLParagraphElement.prototype,
+ "Must not use the HTMLParagraphElement from the realm of the active function object (w.HTMLParagraphElement)");
+
+ return o;
+ }
+
+ const ElementWithDynamicPrototype = new Proxy(TestElement, {
+ get: function (target, name) {
+ if (name == "prototype")
+ return returnNotAnObject ? notAnObject : {};
+ return target[name];
+ }
+ });
+
+ w.customElements.define("test-element", ElementWithDynamicPrototype, { extends: "p" });
+
+ returnNotAnObject = true;
+ new ElementWithDynamicPrototype();
+ }, "If prototype is not object (" + notAnObject + "), derives the fallback from NewTarget's realm (customized built-in elements)");
+
+ test_with_window(w => {
+ // We have to return an object during define(), but not during super()
+ let returnNotAnObject = false;
+
+ function TestElement() {
+ const o = Reflect.construct(w.HTMLParagraphElement, [], new.target);
+
+ assert_equals(Object.getPrototypeOf(o), window.HTMLParagraphElement.prototype,
+ "Must use the HTMLParagraphElement from the realm of NewTarget");
+ assert_not_equals(Object.getPrototypeOf(o), w.HTMLParagraphElement.prototype,
+ "Must not use the HTMLParagraphElement from the realm of the active function object (w.HTMLParagraphElement)");
+
+ return o;
+ }
+
+ // Create the proxy in the subframe, which should not affect what our
+ // prototype ends up as.
+ const ElementWithDynamicPrototype = new w.Proxy(TestElement, {
+ get: function (target, name) {
+ if (name == "prototype")
+ return returnNotAnObject ? notAnObject : {};
+ return target[name];
+ }
+ });
+
+ w.customElements.define("test-element", ElementWithDynamicPrototype, { extends: "p" });
+
+ returnNotAnObject = true;
+ new ElementWithDynamicPrototype();
+ }, "If prototype is not object (" + notAnObject + "), derives the fallback from NewTarget's GetFunctionRealm (customized built-in elements)");
+});
+
+test_with_window(w => {
+ class SomeCustomElement extends HTMLParagraphElement {};
+ var getCount = 0;
+ var countingProxy = new Proxy(SomeCustomElement, {
+ get: function(target, prop, receiver) {
+ if (prop == "prototype") {
+ ++getCount;
+ }
+ return Reflect.get(target, prop, receiver);
+ }
+ });
+ w.customElements.define("failure-counting-element", countingProxy);
+ // define() gets the prototype of the constructor it's passed, so
+ // reset the counter.
+ getCount = 0;
+ assert_throws_js(TypeError,
+ function () { new countingProxy() },
+ "Should not be able to construct an HTMLParagraphElement not named 'p'");
+ assert_equals(getCount, 0, "Should never have gotten .prototype");
+}, 'HTMLParagraphElement constructor must not get .prototype until it finishes its extends sanity checks, calling proxy constructor directly');
+
+test_with_window(w => {
+ class SomeCustomElement extends HTMLParagraphElement {};
+ var getCount = 0;
+ var countingProxy = new Proxy(SomeCustomElement, {
+ get: function(target, prop, receiver) {
+ if (prop == "prototype") {
+ ++getCount;
+ }
+ return Reflect.get(target, prop, receiver);
+ }
+ });
+ w.customElements.define("failure-counting-element", countingProxy);
+ // define() gets the prototype of the constructor it's passed, so
+ // reset the counter.
+ getCount = 0;
+ assert_throws_js(TypeError,
+ function () { Reflect.construct(HTMLParagraphElement, [], countingProxy) },
+ "Should not be able to construct an HTMLParagraphElement not named 'p'");
+ assert_equals(getCount, 0, "Should never have gotten .prototype");
+}, 'HTMLParagraphElement constructor must not get .prototype until it finishes its extends sanity checks, calling via Reflect');
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/htmlconstructor/newtarget.html b/testing/web-platform/tests/custom-elements/htmlconstructor/newtarget.html
new file mode 100644
index 0000000000..b6e7f34537
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/htmlconstructor/newtarget.html
@@ -0,0 +1,130 @@
+<!DOCTYPE html>
+<title>Custom Elements: [HTMLConstructor] derives prototype from NewTarget</title>
+<meta name="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+<meta name="help" content="https://html.spec.whatwg.org/multipage/dom.html#htmlconstructor">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<body>
+<script>
+"use strict";
+
+test_with_window(w => {
+ let beforeDefinition = true;
+ const proto1 = { "proto": "number one" };
+ const proto2 = { "proto": "number two" };
+
+ function TestElement() {
+ const o = Reflect.construct(w.HTMLElement, [], new.target);
+ assert_equals(Object.getPrototypeOf(o), proto2,
+ "Must use the value returned from new.target.prototype");
+ assert_not_equals(Object.getPrototypeOf(o), proto1,
+ "Must not use the prototype stored at definition time");
+ }
+
+ const ElementWithDynamicPrototype = new Proxy(TestElement, {
+ get: function (target, name) {
+ if (name == "prototype")
+ return beforeDefinition ? proto1 : proto2;
+ return target[name];
+ }
+ });
+
+ w.customElements.define("test-element", ElementWithDynamicPrototype);
+
+ beforeDefinition = false;
+ new ElementWithDynamicPrototype();
+}, "Use NewTarget's prototype, not the one stored at definition time");
+
+test_with_window(w => {
+ // We have to not throw during define(), but throw during super()
+ let throws = false;
+
+ let err = { name: "prototype throws" };
+ function TestElement() {
+ throws = true;
+ assert_throws_exactly(err, () => {
+ Reflect.construct(w.HTMLElement, [], new.target);
+ });
+ }
+
+ const ElementWithDynamicPrototype = new Proxy(TestElement, {
+ get: function (target, name) {
+ if (throws && name == "prototype")
+ throw err;
+ return target[name];
+ }
+ });
+
+ w.customElements.define("test-element", ElementWithDynamicPrototype);
+
+ new ElementWithDynamicPrototype();
+
+}, "Rethrow any exceptions thrown while getting the prototype");
+
+[null, undefined, 5, "string"].forEach(function (notAnObject) {
+ test_with_window(w => {
+ // We have to return an object during define(), but not during super()
+ let returnNotAnObject = false;
+
+ function TestElement() {
+ const o = Reflect.construct(w.HTMLElement, [], new.target);
+
+ assert_equals(Object.getPrototypeOf(new.target), window.Function.prototype);
+ assert_equals(Object.getPrototypeOf(o), window.HTMLElement.prototype,
+ "Must use the HTMLElement from the realm of NewTarget");
+ assert_not_equals(Object.getPrototypeOf(o), w.HTMLElement.prototype,
+ "Must not use the HTMLElement from the realm of the active function object (w.HTMLElement)");
+
+ return o;
+ }
+
+ const ElementWithDynamicPrototype = new Proxy(TestElement, {
+ get: function (target, name) {
+ if (name == "prototype")
+ return returnNotAnObject ? notAnObject : {};
+ return target[name];
+ }
+ });
+
+ w.customElements.define("test-element", ElementWithDynamicPrototype);
+
+ returnNotAnObject = true;
+ new ElementWithDynamicPrototype();
+ }, "If prototype is not object (" + notAnObject + "), derives the fallback from NewTarget's realm (autonomous custom elements)");
+
+ test_with_window(w => {
+ // We have to return an object during define(), but not during super()
+ let returnNotAnObject = false;
+
+ function TestElement() {
+ const o = Reflect.construct(w.HTMLElement, [], new.target);
+
+ assert_equals(Object.getPrototypeOf(new.target), window.Function.prototype);
+ assert_equals(Object.getPrototypeOf(o), window.HTMLElement.prototype,
+ "Must use the HTMLElement from the realm of NewTarget");
+ assert_not_equals(Object.getPrototypeOf(o), w.HTMLElement.prototype,
+ "Must not use the HTMLElement from the realm of the active function object (w.HTMLElement)");
+
+ return o;
+ }
+
+ // Create the proxy in the subframe, which should not affect what our
+ // prototype ends up as.
+ const ElementWithDynamicPrototype = new w.Proxy(TestElement, {
+ get: function (target, name) {
+ if (name == "prototype")
+ return returnNotAnObject ? notAnObject : {};
+ return target[name];
+ }
+ });
+
+ w.customElements.define("test-element", ElementWithDynamicPrototype);
+
+ returnNotAnObject = true;
+ new ElementWithDynamicPrototype();
+ }, "If prototype is not object (" + notAnObject + "), derives the fallback from NewTarget's GetFunctionRealm (autonomous custom elements)");
+});
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/microtasks-and-constructors.html b/testing/web-platform/tests/custom-elements/microtasks-and-constructors.html
new file mode 100644
index 0000000000..78d2cba42c
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/microtasks-and-constructors.html
@@ -0,0 +1,123 @@
+<!DOCTYPE html>
+<title>Custom elements: performing a microtask checkpoint after construction</title>
+<meta name="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/scripting.html#concept-upgrade-an-element">
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-create-element">
+<link rel="help" href="https://github.com/whatwg/html/issues/2381">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="log"></div>
+
+<x-upgrade></x-upgrade>
+
+<script>
+"use strict";
+setup({ allow_uncaught_exception: true });
+
+window.doMicrotasks = (callback1, callback2 = callback1) => {
+ Promise.resolve().then(callback1);
+
+ const mo = new MutationObserver(callback2);
+ const node = document.createTextNode("");
+ mo.observe(node, { characterData: true });
+ node.data = "x";
+};
+
+window.logMicrotasks = events => {
+ window.doMicrotasks(() => events.push("promise microtask"),
+ () => events.push("MutationObserver microtask"));
+};
+
+window.flushAsyncEvents = () => {
+ return new Promise(resolve => step_timeout(resolve, 0));
+};
+
+window.x0Events = [];
+customElements.define("x-0", class extends HTMLElement {
+ constructor() {
+ super();
+ logMicrotasks(window.x0Events);
+ }
+});
+</script>
+
+<x-0></x-0>
+
+<script>
+"use strict";
+
+test(() => {
+ assert_array_equals(window.x0Events, ["promise microtask", "MutationObserver microtask"]);
+}, "Microtasks evaluate immediately when the stack is empty inside the parser");
+
+customElements.define("x-bad", class extends HTMLElement {
+ constructor() {
+ super();
+ doMicrotasks(() => this.setAttribute("attribute", "value"));
+ }
+});
+</script>
+
+<x-bad></x-bad>
+
+<script>
+"use strict";
+
+test(() => {
+ const xBad = document.querySelector("x-bad");
+ assert_false(xBad.hasAttribute("attribute"), "The attribute must not be present");
+ assert_true(xBad instanceof HTMLUnknownElement, "The element must be a HTMLUnknownElement");
+}, "Microtasks evaluate immediately when the stack is empty inside the parser, causing the " +
+ "checks on no attributes to fail")
+
+promise_test(() => {
+ const events = [];
+ customElements.define("x-1", class extends HTMLElement {
+ constructor() {
+ super();
+ logMicrotasks(events);
+ }
+ });
+
+ document.createElement("x-1");
+ events.push("after");
+
+ return flushAsyncEvents().then(() => {
+ assert_array_equals(events, ["after", "promise microtask", "MutationObserver microtask"]);
+ });
+}, "Microtasks evaluate afterward when the stack is not empty using createElement()");
+
+promise_test(() => {
+ const events = [];
+ class X2 extends HTMLElement {
+ constructor() {
+ super();
+ logMicrotasks(events);
+ }
+ }
+ customElements.define("x-2", X2);
+
+ new X2();
+ events.push("after");
+
+ return flushAsyncEvents().then(() => {
+ assert_array_equals(events, ["after", "promise microtask", "MutationObserver microtask"]);
+ });
+}, "Microtasks evaluate afterward when the stack is not empty using the constructor");
+
+promise_test(() => {
+ const events = [];
+ customElements.define("x-upgrade", class extends HTMLElement {
+ constructor() {
+ super();
+ logMicrotasks(events);
+ }
+ });
+ events.push("after");
+
+ return flushAsyncEvents().then(() => {
+ assert_array_equals(events, ["after", "promise microtask", "MutationObserver microtask"]);
+ });
+}, "Microtasks evaluate afterward when the stack is not empty due to upgrades");
+</script>
diff --git a/testing/web-platform/tests/custom-elements/overwritten-customElements-global.html b/testing/web-platform/tests/custom-elements/overwritten-customElements-global.html
new file mode 100644
index 0000000000..dcb97b6652
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/overwritten-customElements-global.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+<script>
+"use strict";
+
+test(() => {
+ class SomeElement1 extends HTMLElement {}
+ customElements.define("some-element-1", SomeElement1);
+
+ const savedCustomElements = Object.getOwnPropertyDescriptor(window, "customElements");
+ Object.defineProperty(window, "customElements", { value: {}, configurable: true });
+
+ const element = document.createElement("some-element-1");
+ assert_true(element instanceof SomeElement1);
+
+ Object.defineProperty(window, "customElements", savedCustomElements);
+}, "Custom elements can still be created after `window.customElements` is overwritten.");
+
+test(() => {
+ class SomeElement2 extends HTMLElement {}
+ customElements.define("some-element-2", SomeElement2);
+
+ const savedCustomElements = Object.getOwnPropertyDescriptor(window, "customElements");
+ Object.defineProperty(window, "customElements", { value: {}, configurable: true });
+
+ const element = new SomeElement2();
+ assert_true(element instanceof SomeElement2);
+
+ Object.defineProperty(window, "customElements", savedCustomElements);
+}, "Custom elements can still be constructed after `window.customElements` is overwritten.");
+
+test(() => {
+ class SomeElement3 extends HTMLElement {}
+ customElements.define("some-element-3", SomeElement3);
+
+ const savedCustomElements = Object.getOwnPropertyDescriptor(window, "customElements");
+ delete window.customElements;
+
+ const element = document.createElement("some-element-3");
+ assert_true(element instanceof SomeElement3);
+
+ Object.defineProperty(window, "customElements", savedCustomElements);
+}, "Custom elements can still be created after `window.customElements` is deleted.");
+
+test(() => {
+ class SomeElement4 extends HTMLElement {}
+ customElements.define("some-element-4", SomeElement4);
+
+ const savedCustomElements = Object.getOwnPropertyDescriptor(window, "customElements");
+ delete window.customElements;
+
+ const element = new SomeElement4();
+ assert_true(element instanceof SomeElement4);
+
+ Object.defineProperty(window, "customElements", savedCustomElements);
+}, "Custom elements can still be constructed after `window.customElements` is deleted.");
+</script>
diff --git a/testing/web-platform/tests/custom-elements/parser/parser-constructs-custom-element-in-document-write.html b/testing/web-platform/tests/custom-elements/parser/parser-constructs-custom-element-in-document-write.html
new file mode 100644
index 0000000000..14c830b9ba
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/parser/parser-constructs-custom-element-in-document-write.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: Changes to the HTML parser</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="HTML parser must construct custom elements inside document.write">
+<link rel="help" href="https://html.spec.whatwg.org/#create-an-element-for-the-token">
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-create-element">
+<link rel="help" href="https://html.spec.whatwg.org/#document.write()">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+var numberOfChildNodesInConnectedCallback = 0;
+
+class MyCustomElement extends HTMLElement {
+ connectedCallback() {
+ numberOfChildNodesInConnectedCallback = this.childNodes.length;
+ }
+}
+customElements.define('my-custom-element', MyCustomElement);
+
+document.write('<my-custom-element>hello <b>world</b></my-custom-element>');
+
+test(function () {
+ var instance = document.querySelector('my-custom-element');
+
+ assert_true(instance instanceof HTMLElement);
+ assert_true(instance instanceof MyCustomElement);
+
+}, 'HTML parser must instantiate custom elements inside document.write');
+
+test(function () {
+ assert_equals(numberOfChildNodesInConnectedCallback, 0);
+
+}, 'HTML parser should call connectedCallback before appending child nodes inside document.write');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/parser/parser-constructs-custom-element-synchronously.html b/testing/web-platform/tests/custom-elements/parser/parser-constructs-custom-element-synchronously.html
new file mode 100644
index 0000000000..206aa17f20
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/parser/parser-constructs-custom-element-synchronously.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: Changes to the HTML parser</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="HTML parser must construct a custom element synchronously">
+<link rel="help" href="https://html.spec.whatwg.org/#create-an-element-for-the-token">
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-create-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+var childElementCountInConstructor;
+var containerChildNodesInConstructor = [];
+var containerNextSiblingInConstructor;
+class MyCustomElement extends HTMLElement {
+ constructor() {
+ super();
+ var container = document.getElementById('custom-element-container');
+ for (var i = 0; i < container.childNodes.length; i++)
+ containerChildNodesInConstructor.push(container.childNodes[i]);
+ containerNextSiblingInConstructor = container.nextSibling;
+ }
+};
+customElements.define('my-custom-element', MyCustomElement);
+
+</script>
+<div id="custom-element-container">
+ <span id="custom-element-previous-element"></span>
+ <my-custom-element></my-custom-element>
+ <div id="custom-element-next-element"></div>
+</div>
+<script>
+
+test(function () {
+ var instance = document.querySelector('my-custom-element');
+
+ assert_equals(containerChildNodesInConstructor.length, 3);
+ assert_equals(containerChildNodesInConstructor[0], instance.parentNode.firstChild);
+ assert_equals(containerChildNodesInConstructor[1], document.getElementById('custom-element-previous-element'));
+ assert_equals(containerChildNodesInConstructor[2], instance.previousSibling);
+ assert_equals(containerNextSiblingInConstructor, null);
+
+}, 'HTML parser must only append nodes that appear before a custom element before instantiating the custom element');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/parser/parser-constructs-custom-elements-with-is.html b/testing/web-platform/tests/custom-elements/parser/parser-constructs-custom-elements-with-is.html
new file mode 100644
index 0000000000..96c00278a3
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/parser/parser-constructs-custom-elements-with-is.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: Changes to the HTML parser</title>
+<meta name="author" title="John Dai" href="mailto:jdai@mozilla.com">
+<meta name="assert" content="HTML parser creates a custom element which contains is attribute">
+<link rel="help" href="https://html.spec.whatwg.org/#create-an-element-for-the-token">
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-create-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<autonomous-custom-element id="instance1" is="is-custom-element"></autonomous-custom-element>
+<script>
+
+class AutonomousCustomElement extends HTMLElement { };
+class IsCustomElement extends HTMLElement { };
+
+customElements.define('autonomous-custom-element', AutonomousCustomElement);
+customElements.define('is-custom-element', IsCustomElement);
+
+test(function () {
+ var customElement = document.getElementById('instance1');
+
+ assert_true(customElement instanceof HTMLElement, 'A resolved custom element must be an instance of HTMLElement');
+ assert_false(customElement instanceof HTMLUnknownElement, 'A resolved custom element must NOT be an instance of HTMLUnknownElement');
+ assert_true(customElement instanceof AutonomousCustomElement, 'A resolved custom element must be an instance of that custom element');
+ assert_equals(customElement.localName, 'autonomous-custom-element');
+ assert_equals(customElement.namespaceURI, 'http://www.w3.org/1999/xhtml', 'A custom element HTML must use HTML namespace');
+
+}, 'HTML parser must create a defined autonomous custom element when customElements.define comes after HTML parser creation');
+
+</script>
+<autonomous-custom-element id="instance2" is="is-custom-element"></autonomous-custom-element>
+<script>
+
+test(function () {
+ var customElement = document.getElementById('instance2');
+
+ assert_true(customElement instanceof HTMLElement, 'A resolved custom element must be an instance of HTMLElement');
+ assert_false(customElement instanceof HTMLUnknownElement, 'A resolved custom element must NOT be an instance of HTMLUnknownElement');
+ assert_true(customElement instanceof AutonomousCustomElement, 'A resolved custom element must be an instance of that custom element');
+ assert_equals(customElement.localName, 'autonomous-custom-element');
+ assert_equals(customElement.namespaceURI, 'http://www.w3.org/1999/xhtml', 'A custom element HTML must use HTML namespace');
+
+}, 'HTML parser must create a defined autonomous custom element when customElements.define comes before HTML parser creation');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/parser/parser-constructs-custom-elements.html b/testing/web-platform/tests/custom-elements/parser/parser-constructs-custom-elements.html
new file mode 100644
index 0000000000..3f13c50a0e
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/parser/parser-constructs-custom-elements.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: Changes to the HTML parser</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="HTML parser creates a custom element">
+<link rel="help" href="https://html.spec.whatwg.org/#create-an-element-for-the-token">
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-create-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<my-custom-element id="instance1"></my-custom-element>
+<script>
+
+class MyCustomElement extends HTMLElement { };
+
+test(function () {
+ var customElement = document.getElementById('instance1');
+
+ assert_true(customElement instanceof HTMLElement, 'An unresolved custom element must be an instance of HTMLElement');
+ assert_false(customElement instanceof MyCustomElement, 'An unresolved custom element must NOT be an instance of that custom element');
+ assert_equals(customElement.localName, 'my-custom-element');
+ assert_equals(customElement.namespaceURI, 'http://www.w3.org/1999/xhtml', 'A custom element HTML must use HTML namespace');
+
+}, 'HTML parser must NOT create a custom element before customElements.define is called');
+
+customElements.define('my-custom-element', MyCustomElement);
+
+</script>
+<my-custom-element id="instance2"></my-custom-element>
+<script>
+
+test(function () {
+ var customElement = document.getElementById('instance2');
+
+ assert_true(customElement instanceof HTMLElement, 'A resolved custom element must be an instance of HTMLElement');
+ assert_false(customElement instanceof HTMLUnknownElement, 'A resolved custom element must NOT be an instance of HTMLUnknownElement');
+ assert_true(customElement instanceof MyCustomElement, 'A resolved custom element must be an instance of that custom element');
+ assert_equals(customElement.localName, 'my-custom-element');
+ assert_equals(customElement.namespaceURI, 'http://www.w3.org/1999/xhtml', 'A custom element HTML must use HTML namespace');
+
+}, 'HTML parser must create a defined custom element before executing inline scripts');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/parser/parser-custom-element-in-foreign-content.html b/testing/web-platform/tests/custom-elements/parser/parser-custom-element-in-foreign-content.html
new file mode 100644
index 0000000000..2ae0f1309c
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/parser/parser-custom-element-in-foreign-content.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<title>Custom Elements: Custom element in foreign content</title>
+<meta name="assert" content="HTML parser should not create non-HTML namespace custom elements">
+<link rel="help" href="https://html.spec.whatwg.org/#create-an-element-for-the-token">
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-create-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+class ThrowsException extends HTMLElement {
+ constructor() {
+ throw 'Bad';
+ }
+};
+customElements.define('throws-exception', ThrowsException);
+</script>
+<svg>
+ <throws-exception/>
+</svg>
+<script>
+test(function () {
+ var instance = document.querySelector('throws-exception');
+ assert_false(instance instanceof ThrowsException,
+ 'The HTML parser must NOT instantiate a custom element in non-HTML namespaces');
+ assert_false(instance instanceof HTMLUnknownElement, 'The HTML parser should not fallback');
+ assert_true(instance instanceof SVGElement,
+ 'The element created by the HTML parser must be an instance of SVGElement');
+}, 'HTML parser should not create custom elements in non-HTML namespaces');
+</script>
diff --git a/testing/web-platform/tests/custom-elements/parser/parser-fallsback-to-unknown-element.html b/testing/web-platform/tests/custom-elements/parser/parser-fallsback-to-unknown-element.html
new file mode 100644
index 0000000000..82e970f1ae
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/parser/parser-fallsback-to-unknown-element.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: Changes to the HTML parser</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="HTML parser must fallback to creating a HTMLUnknownElement when a custom element construction fails">
+<link rel="help" href="https://html.spec.whatwg.org/#create-an-element-for-the-token">
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-create-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+setup({allow_uncaught_exception:true});
+
+class ReturnsTextNode extends HTMLElement {
+ constructor() {
+ super();
+ return document.createTextNode('some text');
+ }
+};
+customElements.define('returns-text', ReturnsTextNode);
+
+class ReturnsNonElementObject extends HTMLElement {
+ constructor() {
+ super();
+ return {};
+ }
+};
+customElements.define('returns-non-element-object', ReturnsNonElementObject);
+
+class LacksSuperCall extends HTMLElement {
+ constructor() { }
+};
+customElements.define('lacks-super-call', LacksSuperCall);
+
+class ThrowsException extends HTMLElement {
+ constructor() {
+ throw 'Bad';
+ }
+};
+customElements.define('throws-exception', ThrowsException);
+
+</script>
+<returns-text></returns-text>
+<returns-non-element-object></returns-non-element-object>
+<lacks-super-call></lacks-super-call>
+<throws-exception></throws-exception>
+<script>
+
+test(function () {
+ var instance = document.querySelector('returns-text');
+
+ assert_false(instance instanceof ReturnsTextNode, 'HTML parser must NOT instantiate a custom element when the constructor returns a Text node');
+ assert_true(instance instanceof HTMLElement, 'The fallback element created by HTML parser must be an instance of HTMLElement');
+ assert_true(instance instanceof HTMLUnknownElement, 'The fallback element created by HTML parser must be an instance of HTMLUnknownElement');
+
+}, 'HTML parser must create a fallback HTMLUnknownElement when a custom element constructor returns a Text node');
+
+test(function () {
+ var instance = document.querySelector('returns-non-element-object');
+
+ assert_false(instance instanceof ReturnsNonElementObject, 'HTML parser must NOT instantiate a custom element when the constructor returns a non-Element object');
+ assert_true(instance instanceof HTMLElement, 'The fallback element created by HTML parser must be an instance of HTMLElement');
+ assert_true(instance instanceof HTMLUnknownElement, 'The fallback element created by HTML parser must be an instance of HTMLUnknownElement');
+
+}, 'HTML parser must create a fallback HTMLUnknownElement when a custom element constructor returns non-Element object');
+
+test(function () {
+ var instance = document.querySelector('lacks-super-call');
+
+ assert_false(instance instanceof LacksSuperCall, 'HTML parser must NOT instantiate a custom element when the constructor does not call super()');
+ assert_true(instance instanceof HTMLElement, 'The fallback element created by HTML parser must be an instance of HTMLElement');
+ assert_true(instance instanceof HTMLUnknownElement, 'The fallback element created by HTML parser must be an instance of HTMLUnknownElement');
+
+}, 'HTML parser must create a fallback HTMLUnknownElement when a custom element constructor does not call super()');
+
+test(function () {
+ var instance = document.querySelector('throws-exception');
+
+ assert_false(instance instanceof ThrowsException, 'HTML parser must NOT instantiate a custom element when the constructor throws an exception');
+ assert_true(instance instanceof HTMLElement, 'The fallback element created by HTML parser must be an instance of HTMLElement');
+ assert_true(instance instanceof HTMLUnknownElement, 'The fallback element created by HTML parser must be an instance of HTMLUnknownElement');
+
+}, 'HTML parser must create a fallback HTMLUnknownElement when a custom element constructor throws an exception');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/parser/parser-sets-attributes-and-children.html b/testing/web-platform/tests/custom-elements/parser/parser-sets-attributes-and-children.html
new file mode 100644
index 0000000000..987bf8525f
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/parser/parser-sets-attributes-and-children.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: Changes to the HTML parser</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="HTML parser must set the attributes and append the children on a custom element">
+<link rel="help" href="https://html.spec.whatwg.org/#create-an-element-for-the-token">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/parsing.html#insert-a-foreign-element">
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-create-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+var numberOfAttributesInConstructor = 0;
+var numberOfChildNodesInConstructor = 0;
+var numberOfChildNodesInAttributeChangedCallback = 0;
+var numberOfChildNodesInConnectedCallback = 0;
+var attributesChangedCalls = [];
+
+class MyCustomElement extends HTMLElement {
+ constructor(...args) {
+ super(...args);
+ numberOfAttributesInConstructor = this.attributes.length;
+ numberOfChildNodesInConstructor = this.childNodes.length;
+ }
+
+ attributeChangedCallback(...args) {
+ attributesChangedCalls.push(create_attribute_changed_callback_log(this, ...args));
+ numberOfChildNodesInAttributeChangedCallback = this.childNodes.length;
+ }
+
+ static get observedAttributes() {
+ return ['id', 'class'];
+ }
+
+ connectedCallback() {
+ numberOfChildNodesInConnectedCallback = this.childNodes.length;
+ }
+};
+customElements.define('my-custom-element', MyCustomElement);
+
+</script>
+<my-custom-element id="custom-element-id" class="class1 class2">hello <b>world</b></my-custom-element>
+<script>
+
+var customElement = document.querySelector('my-custom-element');
+
+test(function () {
+ assert_equals(customElement.getAttribute('id'), 'custom-element-id', 'HTML parser must preserve the id attribute');
+ assert_equals(customElement.id, 'custom-element-id', 'HTML parser must preserve the semantics of reflect for the id attribute');
+ assert_equals(customElement.getAttribute('class'), 'class1 class2', 'HTML parser must preserve the class attribute');
+ assert_equals(customElement.classList.length, 2, 'HTML parser must initialize classList on custom elements');
+ assert_equals(customElement.classList[0], 'class1', 'HTML parser must initialize classList on custom elements');
+ assert_equals(customElement.classList[1], 'class2', 'HTML parser must initialize classList on custom elements');
+}, 'HTML parser must set the attributes');
+
+test(function () {
+ assert_equals(customElement.childNodes.length, 2, 'HTML parser must append child nodes');
+ assert_true(customElement.firstChild instanceof Text, 'HTML parser must append Text node child to a custom element');
+ assert_equals(customElement.firstChild.data, 'hello ', 'HTML parser must append Text node child to a custom element');
+ assert_true(customElement.lastChild instanceof HTMLElement, 'HTML parser must append a builtin element child to a custom element');
+ assert_true(customElement.lastChild.firstChild instanceof Text, 'HTML parser must preserve grandchild nodes of a custom element');
+ assert_equals(customElement.lastChild.firstChild.data, 'world', 'HTML parser must preserve grandchild nodes of a custom element');
+}, 'HTML parser must append child nodes');
+
+test(function () {
+ assert_equals(numberOfAttributesInConstructor, 0, 'HTML parser must not set attributes on a custom element before invoking the constructor');
+ assert_equals(numberOfChildNodesInConstructor, 0, 'HTML parser must not append child nodes to a custom element before invoking the constructor');
+}, 'HTML parser must set the attributes or append children before calling constructor');
+
+test(function () {
+ // https://html.spec.whatwg.org/multipage/parsing.html#insert-a-foreign-element
+ // 3.3. Pop the element queue from the custom element reactions
+ // stack, and invoke custom element reactions in that queue.
+ assert_equals(numberOfChildNodesInConnectedCallback, 0);
+}, 'HTML parser should call connectedCallback before appending child nodes.');
+
+test(function () {
+ assert_equals(attributesChangedCalls.length, 2);
+ assert_attribute_log_entry(attributesChangedCalls[0], {name: 'id', oldValue: null, newValue: 'custom-element-id', namespace: null});
+ assert_attribute_log_entry(attributesChangedCalls[1], {name: 'class', oldValue: null, newValue: 'class1 class2', namespace: null});
+ // https://html.spec.whatwg.org/multipage/parsing.html#create-an-element-for-the-token
+ // 9.2. Invoke custom element reactions in queue.
+ assert_equals(numberOfChildNodesInAttributeChangedCallback, 0,
+ 'attributeChangedCallback should be called ' +
+ 'before appending a child');
+}, 'HTML parser must enqueue attributeChanged reactions');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/parser/parser-uses-constructed-element.html b/testing/web-platform/tests/custom-elements/parser/parser-uses-constructed-element.html
new file mode 100644
index 0000000000..dd98f15cd9
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/parser/parser-uses-constructed-element.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: HTML parser must construct a custom element instead of upgrading</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="HTML parser must construct a custom element instead of upgrading">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="help" href="https://html.spec.whatwg.org/#create-an-element-for-the-token">
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-create-element">
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+let anotherElementCreatedBeforeSuperCall = undefined;
+let elementCreatedBySuperCall = undefined;
+let shouldCreateElementBeforeSuperCall = true;
+class InstantiatesItselfBeforeSuper extends HTMLElement {
+ constructor() {
+ if (shouldCreateElementBeforeSuperCall) {
+ shouldCreateElementBeforeSuperCall = false;
+ anotherElementCreatedBeforeSuperCall = new InstantiatesItselfBeforeSuper();
+ }
+ super();
+ elementCreatedBySuperCall = this;
+ }
+};
+customElements.define('instantiates-itself-before-super', InstantiatesItselfBeforeSuper);
+
+let shouldCreateAnotherInstance = true;
+let anotherInstance = undefined;
+let firstInstance = undefined;
+class ReturnsAnotherInstance extends HTMLElement {
+ constructor() {
+ super();
+ if (shouldCreateAnotherInstance) {
+ shouldCreateAnotherInstance = false;
+ firstInstance = this;
+ anotherInstance = new ReturnsAnotherInstance;
+ return anotherInstance;
+ } else
+ return this;
+ }
+};
+customElements.define('returns-another-instance', ReturnsAnotherInstance);
+
+</script>
+<instantiates-itself-before-super></instantiates-itself-before-super>
+<returns-another-instance></returns-another-instance>
+<script>
+
+test(function () {
+ var instance = document.querySelector('instantiates-itself-before-super');
+
+ assert_true(instance instanceof InstantiatesItselfBeforeSuper, 'HTML parser must insert the synchronously constructed custom element');
+ assert_equals(instance, elementCreatedBySuperCall, 'HTML parser must insert the element returned by the custom element constructor');
+ assert_not_equals(instance, anotherElementCreatedBeforeSuperCall, 'HTML parser must not insert another instance of the custom element created before super() call');
+ assert_equals(anotherElementCreatedBeforeSuperCall.parentNode, null, 'HTML parser must not insert another instance of the custom element created before super() call');
+
+}, 'HTML parser must use the returned value of the custom element constructor instead of the one created before super() call');
+
+test(function () {
+ var instance = document.querySelector('returns-another-instance');
+
+ assert_true(instance instanceof ReturnsAnotherInstance, 'HTML parser must insert the synchronously constructed custom element');
+ assert_equals(instance, anotherInstance, 'HTML parser must insert the element returned by the custom element constructor');
+ assert_not_equals(instance, firstInstance, 'HTML parser must not insert the element created by super() call if the constructor returned another element');
+ assert_equals(firstInstance.parentNode, null, 'HTML parser must not insert the element created by super() call if the constructor returned another element');
+
+}, 'HTML parser must use the returned value of the custom element constructor instead using the one created in super() call');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/parser/parser-uses-create-an-element-for-a-token-svg.svg b/testing/web-platform/tests/custom-elements/parser/parser-uses-create-an-element-for-a-token-svg.svg
new file mode 100644
index 0000000000..526de0f63f
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/parser/parser-uses-create-an-element-for-a-token-svg.svg
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg:svg xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/1999/xhtml"
+ width="100%" height="100%" viewBox="0 0 800 600">
+<svg:title>XML parser should use "create an element for a token"</svg:title>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/xhtml.html#parsing-xhtml-documents:create-an-element-for-the-token"/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script><![CDATA[
+class MyElement1 extends HTMLElement {}
+customElements.define('my-element', MyElement1);
+class MyElement2 extends HTMLDivElement {}
+customElements.define('my-div', MyElement2, { extends: 'div' });
+
+var test1 = async_test('XML parser should create autonomous custom elements.');
+window.addEventListener('load', test1.step_func_done(() => {
+ assert_true(document.getElementById('me1') instanceof MyElement1);
+}));
+
+var test2 = async_test('XML parser should create custom built-in elements.');
+window.addEventListener('load', test2.step_func_done(() => {
+ assert_true(document.getElementById('me2') instanceof MyElement2);
+}));
+]]></script>
+<my-element id="me1"></my-element>
+<div is="my-div" id="me2"></div>
+</svg:svg>
diff --git a/testing/web-platform/tests/custom-elements/parser/parser-uses-registry-of-owner-document.html b/testing/web-platform/tests/custom-elements/parser/parser-uses-registry-of-owner-document.html
new file mode 100644
index 0000000000..bb256da295
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/parser/parser-uses-registry-of-owner-document.html
@@ -0,0 +1,154 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: HTML parser must use the owner document's custom element registry</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="HTML parser must use the owner document's custom element registry">
+<link rel="help" href="https://html.spec.whatwg.org/#create-an-element-for-the-token">
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-create-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+class MyCustomElement extends HTMLElement { };
+customElements.define('my-custom-element', MyCustomElement);
+
+document.write('<template><my-custom-element></my-custom-element></template>');
+
+test(function () {
+ var template = document.querySelector('template');
+ var instance = template.content.firstChild;
+
+ assert_true(instance instanceof HTMLElement,
+ 'A custom element inside a template element must be an instance of HTMLElement');
+ assert_false(instance instanceof MyCustomElement,
+ 'A custom element must not be instantiated inside a template element using the registry of the template element\'s owner document');
+ assert_equals(instance.ownerDocument, template.content.ownerDocument,
+ 'Custom elements inside a template must use the appropriate template contents owner document as the owner document');
+
+}, 'HTML parser must not instantiate custom elements inside template elements');
+
+var iframe = document.createElement('iframe');
+document.body.appendChild(iframe);
+iframe.contentDocument.body.innerHTML = '<my-custom-element></my-custom-element>';
+
+test(function () {
+ var instance = iframe.contentDocument.querySelector('my-custom-element');
+
+ assert_true(instance instanceof iframe.contentWindow.HTMLElement);
+ assert_false(instance instanceof MyCustomElement);
+
+}, 'HTML parser must not use the registry of the owner element\'s document inside an iframe');
+
+class ElementInIFrame extends iframe.contentWindow.HTMLElement { };
+iframe.contentWindow.customElements.define('element-in-iframe', ElementInIFrame);
+iframe.contentDocument.body.innerHTML = '<element-in-iframe></element-in-iframe>';
+
+test(function () {
+ var instance = iframe.contentDocument.querySelector('element-in-iframe');
+
+ assert_true(instance instanceof iframe.contentWindow.HTMLElement, 'A custom element inside an iframe must be an instance of HTMLElement');
+ assert_true(instance instanceof ElementInIFrame,
+ 'A custom element must be instantiated inside an iframe using the registry of the content document');
+ assert_equals(instance.ownerDocument, iframe.contentDocument,
+ 'The owner document of custom elements inside an iframe must be the content document of the iframe');
+
+}, 'HTML parser must use the registry of the content document inside an iframe');
+
+document.write('<element-in-iframe></element-in-iframe>');
+
+test(function () {
+ var instance = document.querySelector('element-in-iframe');
+
+ assert_true(instance instanceof HTMLElement);
+ assert_false(instance instanceof ElementInIFrame);
+
+}, 'HTML parser must not instantiate a custom element defined inside an frame in frame element\'s owner document');
+
+document.body.removeChild(iframe);
+
+test(function () {
+ var windowlessDocument = (new DOMParser()).parseFromString('<my-custom-element></my-custom-element>', "text/html");
+
+ var instance = windowlessDocument.querySelector('my-custom-element');
+
+ assert_true(instance instanceof HTMLElement);
+ assert_false(instance instanceof MyCustomElement);
+
+}, 'HTML parser must use the registry of window.document in a document created by DOMParser');
+
+test(function () {
+ var windowlessDocument = document.implementation.createDocument ('http://www.w3.org/1999/xhtml', 'html', null);
+ windowlessDocument.documentElement.innerHTML = '<my-custom-element></my-custom-element>';
+
+ var instance = windowlessDocument.querySelector('my-custom-element');
+ assert_true(instance instanceof HTMLElement);
+ assert_false(instance instanceof MyCustomElement);
+
+}, 'HTML parser must use the registry of window.document in a document created by document.implementation.createXHTMLDocument()');
+
+test(function () {
+ var windowlessDocument = new Document;
+ windowlessDocument.appendChild(windowlessDocument.createElement('html'));
+ windowlessDocument.documentElement.innerHTML = '<my-custom-element></my-custom-element>';
+
+ var instance = windowlessDocument.querySelector('my-custom-element');
+
+ assert_true(instance instanceof Element);
+ assert_false(instance instanceof MyCustomElement);
+
+}, 'HTML parser must use the registry of window.document in a document created by new Document');
+
+promise_test(function () {
+ return new Promise(function (resolve, reject) {
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', '../resources/my-custom-element-html-document.html');
+ xhr.overrideMimeType('text/xml');
+ xhr.onload = function () { resolve(xhr.responseXML); }
+ xhr.onerror = function () { reject('Failed to fetch the document'); }
+ xhr.send();
+ }).then(function (doc) {
+ var instance = doc.querySelector('my-custom-element');
+ assert_true(instance instanceof Element);
+ assert_false(instance instanceof MyCustomElement);
+
+ doc.documentElement.innerHTML = '<my-custom-element></my-custom-element>';
+ var instance2 = doc.querySelector('my-custom-element');
+ assert_true(instance2 instanceof Element);
+ assert_false(instance2 instanceof MyCustomElement);
+ });
+}, 'HTML parser must use the registry of window.document in a document created by XMLHttpRequest');
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'my-custom-element', []);
+ // document-open-steps spec doesn't do anything with the custom element
+ // registry, so it should just stick around.
+ contentDocument.write('<my-custom-element></my-custom-element>');
+
+ var instance = contentDocument.querySelector('my-custom-element');
+
+ assert_true(instance instanceof contentWindow.HTMLElement);
+ assert_true(instance instanceof element.class);
+
+}, 'document.write() must not instantiate a custom element without a defined insertion point');
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'my-custom-element', []);
+ // document-open-steps spec doesn't do anything with the custom element
+ // registry, so it should just stick around.
+ contentDocument.writeln('<my-custom-element></my-custom-element>');
+
+ var instance = contentDocument.querySelector('my-custom-element');
+
+ assert_true(instance instanceof contentWindow.HTMLElement);
+ assert_true(instance instanceof element.class);
+
+}, 'document.writeln() must not instantiate a custom element without a defined insertion point');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/parser/serializing-html-fragments-customized-builtins.html b/testing/web-platform/tests/custom-elements/parser/serializing-html-fragments-customized-builtins.html
new file mode 100644
index 0000000000..6992dd6df6
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/parser/serializing-html-fragments-customized-builtins.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/parsing.html#serialising-html-fragments">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<div id="container"></div>
+<script>
+test(() => {
+ class MyParagraph extends HTMLParagraphElement {}
+ customElements.define('my-p', MyParagraph, { extends: 'p' });
+
+ let p = new MyParagraph();
+ p.setAttribute('class', 'foo');
+ assert_equals(p.outerHTML, '<p is="my-p" class="foo"></p>');
+
+ let container = document.querySelector('#container');
+ container.appendChild(p);
+ container.innerHTML = container.innerHTML;
+ assert_not_equals(container.firstChild, p);
+ assert_true(container.firstChild instanceof MyParagraph);
+}, '"is" value should be serialized if the custom element has no "is" content attribute');
+
+test(() => {
+ let p = document.createElement('p', { is: 'your-p' });
+ assert_equals(p.outerHTML, '<p is="your-p"></p>');
+}, '"is" value should be serialized even for an undefined element');
+
+test(() => {
+ class MyDiv extends HTMLDivElement {}
+ customElements.define('my-div', MyDiv, { extends: 'div' });
+
+ let div = document.createElement('div', { is: 'my-div' });
+ div.setAttribute('is', 'foo"bar\n');
+ assert_equals(div.outerHTML, '<div is="foo&quot;bar\n"></div>');
+}, '"is" content attribute should be serialized even if the element is a customized built-in element');
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/perform-microtask-checkpoint-before-construction-xml-parser.xhtml b/testing/web-platform/tests/custom-elements/perform-microtask-checkpoint-before-construction-xml-parser.xhtml
new file mode 100644
index 0000000000..63bb1d143f
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/perform-microtask-checkpoint-before-construction-xml-parser.xhtml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8"?>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<title>Custom Elements: create an element for a token must perform a microtask checkpoint</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org" />
+<meta name="assert" content="When the HTML parser creates an element for a token, it must perform a microtask checkpoint before invoking the constructor" />
+<meta name="help" content="https://html.spec.whatwg.org/multipage/parsing.html#create-an-element-for-the-token" />
+<meta name="help" content="https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/custom-elements-helpers.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+<![CDATA[
+
+async function construct_custom_element_in_parser(test, markup)
+{
+ const window = await create_window_in_test_async(test, 'application/xml', `<?xml version="1.0" encoding="utf-8"?>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<body><script>
+class SomeElement extends HTMLElement {
+ constructor() {
+ super();
+ window.recordsListInConstructor = recordsList.map((records) => records.slice(0));
+ }
+}
+customElements.define('some-element', SomeElement);
+
+const recordsList = [];
+const observer = new MutationObserver((records) => {
+ recordsList.push(records);
+});
+observer.observe(document.body, {childList: true, subtree: true});
+
+window.onload = () => {
+ window.recordsListInDOMContentLoaded = recordsList.map((records) => records.slice(0));
+}
+
+</scr` + `ipt>${markup}</body></html>`);
+ return window;
+}
+
+promise_test(async function () {
+ const contentWindow = await construct_custom_element_in_parser(this, '<b><some-element></some-element></b>');
+ const contentDocument = contentWindow.document;
+
+ let recordsList = contentWindow.recordsListInConstructor;
+ assert_true(Array.isArray(recordsList));
+ assert_equals(recordsList.length, 1);
+ assert_true(Array.isArray(recordsList[0]));
+ assert_equals(recordsList[0].length, 1);
+ let record = recordsList[0][0];
+ assert_equals(record.type, 'childList');
+ assert_equals(record.target, contentDocument.body);
+ assert_equals(record.previousSibling, contentDocument.querySelector('script'));
+ assert_equals(record.nextSibling, null);
+ assert_equals(record.removedNodes.length, 0);
+ assert_equals(record.addedNodes.length, 1);
+ assert_equals(record.addedNodes[0], contentDocument.querySelector('b'));
+
+ recordsList = contentWindow.recordsListInDOMContentLoaded;
+ assert_true(Array.isArray(recordsList));
+ assert_equals(recordsList.length, 2);
+ assert_true(Array.isArray(recordsList[1]));
+ assert_equals(recordsList[1].length, 1);
+ record = recordsList[1][0];
+ assert_equals(record.type, 'childList');
+ assert_equals(record.target, contentDocument.querySelector('b'));
+ assert_equals(record.previousSibling, null);
+ assert_equals(record.nextSibling, null);
+ assert_equals(record.removedNodes.length, 0);
+ assert_equals(record.addedNodes.length, 1);
+ assert_equals(record.addedNodes[0], contentDocument.querySelector('some-element'));
+}, 'XML parser must perform a microtask checkpoint before constructing a custom element');
+
+]]>
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/perform-microtask-checkpoint-before-construction.html b/testing/web-platform/tests/custom-elements/perform-microtask-checkpoint-before-construction.html
new file mode 100644
index 0000000000..83d823ad9f
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/perform-microtask-checkpoint-before-construction.html
@@ -0,0 +1,143 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: create an element for a token must perform a microtask checkpoint</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="When the HTML parser creates an element for a token, it must perform a microtask checkpoint before invoking the constructor">
+<meta name="help" content="https://html.spec.whatwg.org/multipage/parsing.html#create-an-element-for-the-token">
+<meta name="help" content="https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint">
+<meta name="help" content="https://html.spec.whatwg.org/multipage/parsing.html#adoption-agency-algorithm">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/custom-elements-helpers.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+async function construct_custom_element_in_parser(test, markup)
+{
+ const window = await create_window_in_test(test, `
+<!DOCTYPE html>
+<html>
+<body><script>
+class SomeElement extends HTMLElement {
+ constructor() {
+ super();
+ window.recordsListInConstructor = recordsList.map((records) => records.slice(0));
+ }
+}
+customElements.define('some-element', SomeElement);
+
+const recordsList = [];
+const observer = new MutationObserver((records) => {
+ recordsList.push(records);
+});
+observer.observe(document.body, {childList: true, subtree: true});
+
+window.onload = () => {
+ window.recordsListInDOMContentLoaded = recordsList.map((records) => records.slice(0));
+}
+
+</scr` + `ipt>${markup}</body></html>`);
+ return window;
+}
+
+promise_test(async function () {
+ const contentWindow = await construct_custom_element_in_parser(this, '<b><some-element></b>');
+ const contentDocument = contentWindow.document;
+
+ let recordsList = contentWindow.recordsListInConstructor;
+ assert_true(Array.isArray(recordsList));
+ assert_equals(recordsList.length, 1);
+ assert_true(Array.isArray(recordsList[0]));
+ assert_equals(recordsList[0].length, 1);
+ let record = recordsList[0][0];
+ assert_equals(record.type, 'childList');
+ assert_equals(record.target, contentDocument.body);
+ assert_equals(record.previousSibling, contentDocument.querySelector('script'));
+ assert_equals(record.nextSibling, null);
+ assert_equals(record.removedNodes.length, 0);
+ assert_equals(record.addedNodes.length, 1);
+ assert_equals(record.addedNodes[0], contentDocument.querySelector('b'));
+
+ recordsList = contentWindow.recordsListInDOMContentLoaded;
+ assert_true(Array.isArray(recordsList));
+ assert_equals(recordsList.length, 2);
+ assert_true(Array.isArray(recordsList[1]));
+ assert_equals(recordsList[1].length, 1);
+ record = recordsList[1][0];
+ assert_equals(record.type, 'childList');
+ assert_equals(record.target, contentDocument.querySelector('b'));
+ assert_equals(record.previousSibling, null);
+ assert_equals(record.nextSibling, null);
+ assert_equals(record.removedNodes.length, 0);
+ assert_equals(record.addedNodes.length, 1);
+ assert_equals(record.addedNodes[0], contentDocument.querySelector('some-element'));
+}, 'HTML parser must perform a microtask checkpoint before constructing a custom element');
+
+promise_test(async function () {
+ const contentWindow = await construct_custom_element_in_parser(this, '<b><i>hello</b><some-element>');
+ const contentDocument = contentWindow.document;
+ let recordsList = contentWindow.recordsListInConstructor;
+ assert_true(Array.isArray(recordsList));
+ assert_equals(recordsList.length, 1);
+ assert_true(Array.isArray(recordsList[0]));
+ assert_equals(recordsList[0].length, 4);
+
+ let record = recordsList[0][0];
+ assert_equals(record.type, 'childList');
+ assert_equals(record.target, contentDocument.body);
+ assert_equals(record.previousSibling, contentDocument.querySelector('script'));
+ assert_equals(record.nextSibling, null);
+ assert_equals(record.removedNodes.length, 0);
+ assert_equals(record.addedNodes.length, 1);
+ assert_equals(record.addedNodes[0], contentDocument.querySelector('b'));
+
+ record = recordsList[0][1];
+ assert_equals(record.type, 'childList');
+ assert_equals(record.target, contentDocument.querySelector('b'));
+ assert_equals(record.previousSibling, null);
+ assert_equals(record.nextSibling, null);
+ assert_equals(record.removedNodes.length, 0);
+ assert_equals(record.addedNodes.length, 1);
+ assert_equals(record.addedNodes[0], contentDocument.querySelector('i'));
+
+ record = recordsList[0][2];
+ assert_equals(record.type, 'childList');
+ assert_equals(record.target, contentDocument.querySelector('i'));
+ assert_equals(record.previousSibling, null);
+ assert_equals(record.nextSibling, null);
+ assert_equals(record.removedNodes.length, 0);
+ assert_equals(record.addedNodes.length, 1);
+ assert_equals(record.addedNodes[0].nodeType, Node.TEXT_NODE);
+ assert_equals(record.addedNodes[0].data, "hello");
+
+ record = recordsList[0][3];
+ assert_equals(record.type, 'childList');
+ assert_equals(record.target, contentDocument.body);
+ assert_equals(record.previousSibling, contentDocument.querySelector('b'));
+ assert_equals(record.nextSibling, null);
+ assert_equals(record.removedNodes.length, 0);
+ assert_equals(record.addedNodes.length, 1);
+ assert_equals(record.addedNodes[0], contentDocument.querySelectorAll('i')[1]);
+
+ recordsList = contentWindow.recordsListInDOMContentLoaded;
+ assert_true(Array.isArray(recordsList));
+ assert_equals(recordsList.length, 2);
+ assert_true(Array.isArray(recordsList[1]));
+ assert_equals(recordsList[1].length, 1);
+
+ record = recordsList[1][0];
+ assert_equals(record.type, 'childList');
+ assert_equals(record.target, contentDocument.querySelectorAll('i')[1]);
+ assert_equals(record.previousSibling, null);
+ assert_equals(record.nextSibling, null);
+ assert_equals(record.removedNodes.length, 0);
+ assert_equals(record.addedNodes.length, 1);
+ assert_equals(record.addedNodes[0], contentDocument.querySelector('some-element'));
+}, 'HTML parser must perform a microtask checkpoint before constructing a custom element during the adoption agency algorithm');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/prevent-extensions-crash.html b/testing/web-platform/tests/custom-elements/prevent-extensions-crash.html
new file mode 100644
index 0000000000..ae8b96b413
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/prevent-extensions-crash.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Check for crash at step #12 of HTML Constructors</title>
+<meta name='author' href='mailto:masonf@chromium.org'>
+<link rel='help' href='https://html.spec.whatwg.org/multipage/dom.html#html-element-constructors'>
+<link rel='help' href='https://crbug.com/1197894'>
+
+<uh-oh></uh-oh>
+
+<script type="module">
+ Reflect.preventExtensions(document.querySelector('uh-oh'));
+ customElements.define('uh-oh', class extends HTMLElement {});
+</script>
+
+This test passes if it does not crash.
diff --git a/testing/web-platform/tests/custom-elements/pseudo-class-defined-customized-builtins.html b/testing/web-platform/tests/custom-elements/pseudo-class-defined-customized-builtins.html
new file mode 100644
index 0000000000..cab343533a
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/pseudo-class-defined-customized-builtins.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/semantics-other.html#selector-defined">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="iframe"></iframe>
+<script>
+const testList = [
+ { tag_name: 'abbr', is: 'my-abbr', defined: false },
+ { tag_name: 'p', is: '', defined: false },
+];
+
+// Setup iframe to test the parser.
+const neither = 'rgb(255, 0, 0)';
+const defined = 'rgb(255, 165, 0)';
+const not_defined = 'rgb(0, 0, 255)';
+const iframe = document.getElementById("iframe");
+iframe.srcdoc = `<style>
+ * { color:${neither}; }
+ :defined { color:${defined}; }
+ :not(:defined) { color:${not_defined}; }
+</style>`
+ + testList.map(d => `<${d.tag_name} is="${d.is}"></${d.tag_name}>`).join('');
+setup({ explicit_done: true });
+iframe.onload = () => {
+ const doc = iframe.contentDocument;
+ const doc_without_browsing_context = doc.implementation.createHTMLDocument();
+ for (const data of testList) {
+ // Test elements inserted by parser.
+ test_defined(data.defined, doc.getElementsByTagName(data.tag_name)[0], `<${data.tag_name} is="${data.is}">`);
+
+ // Test DOM createElement() methods.
+ let try_upgrade = !data.defined && (data.is === undefined || data.is.length > 0);
+ test_defined_for_createElement(data.defined, try_upgrade, doc, data.tag_name, data.is);
+
+ // Documents without browsing context should behave the same.
+ test_defined_for_createElement(data.defined, false, doc_without_browsing_context, data.tag_name, data.is, 'Without browsing context: ');
+ }
+
+ done();
+};
+
+function test_defined_for_createElement(defined, should_test_change, doc, tag_name, is, description = '') {
+ let has_is = is !== undefined;
+ // Test document.createElement().
+ let element = doc.createElement(tag_name, { is });
+ doc.body.appendChild(element);
+ test_defined(defined, element, `${description}createElement("${tag_name}", is:"${is}")`);
+
+ // Test document.createElementNS().
+ let html_element = doc.createElementNS('http://www.w3.org/1999/xhtml', tag_name, { is })
+ doc.body.appendChild(html_element);
+ test_defined(defined, html_element, `${description}createElementNS("http://www.w3.org/1999/xhtml", "${tag_name}", is:"${is}")`);
+
+ // If the element namespace is not HTML, it should be "uncustomized"; i.e., "defined".
+ let svg_element = doc.createElementNS('http://www.w3.org/2000/svg', tag_name, { is });
+ doc.body.appendChild(svg_element);
+ test_defined(true, svg_element, `${description}createElementNS("http://www.w3.org/2000/svg", "${tag_name}", is:"${is}")`);
+
+ // Test ":defined" changes when the custom element was defined.
+ if (should_test_change) {
+ let w = doc.defaultView;
+ assert_false(!w, 'defaultView required to test change');
+ w.customElements.define(is, class extends w.HTMLElement {}, { extends: tag_name });
+ test_defined(true, element, `Upgraded ${description}createElement("${tag_name}", is:"${is}")`);
+ test_defined(true, html_element, `Upgraded ${description}createElementNS("http://www.w3.org/1999/xhtml", "${tag_name}", is:"${is}")`);
+ }
+}
+
+function test_defined(expected, element, description) {
+ test(() => {
+ assert_equals(element.matches(':defined'), expected, 'matches(":defined")');
+ assert_equals(element.matches(':not(:defined)'), !expected, 'matches(":not(:defined")');
+ const view = element.ownerDocument.defaultView;
+ if (!view)
+ return;
+ const style = view.getComputedStyle(element);
+ assert_equals(style.color, expected ? defined : not_defined, 'getComputedStyle');
+ }, `${description} should ${expected ? 'be' : 'not be'} :defined`);
+}
+
+</script>
diff --git a/testing/web-platform/tests/custom-elements/pseudo-class-defined-print-ref.html b/testing/web-platform/tests/custom-elements/pseudo-class-defined-print-ref.html
new file mode 100644
index 0000000000..1ed6da5958
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/pseudo-class-defined-print-ref.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<style>
+ div {
+ width: 100px;
+ height: 100px;
+ background: green;
+ }
+</style>
+<div></div>
diff --git a/testing/web-platform/tests/custom-elements/pseudo-class-defined-print.html b/testing/web-platform/tests/custom-elements/pseudo-class-defined-print.html
new file mode 100644
index 0000000000..24e1bb7b03
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/pseudo-class-defined-print.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<link rel="match" href="pseudo-class-defined-print-ref.html">
+<style>
+ custom-element {
+ display: block;
+ width: 100px;
+ height: 100px;
+ background: green;
+ }
+ custom-element:not(:defined) {
+ background: red;
+ }
+</style>
+<custom-element></custom-element>
+<script>
+ customElements.define("custom-element", class extends HTMLElement { });
+</script>
diff --git a/testing/web-platform/tests/custom-elements/pseudo-class-defined.html b/testing/web-platform/tests/custom-elements/pseudo-class-defined.html
new file mode 100644
index 0000000000..89d027adaf
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/pseudo-class-defined.html
@@ -0,0 +1,103 @@
+<!DOCTYPE html>
+<link rel="help" href="https://html.spec.whatwg.org/multipage/semantics-other.html#selector-defined">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe id="iframe"></iframe>
+<script>
+const testList = [
+ { tag_name: 'div', defined: true },
+ { tag_name: 'a-a', defined: false },
+ { tag_name: 'font-face', defined: true },
+];
+
+// Setup iframe to test the parser.
+const neither = 'rgb(255, 0, 0)';
+const defined = 'rgb(255, 165, 0)';
+const not_defined = 'rgb(0, 0, 255)';
+const iframe = document.getElementById("iframe");
+iframe.srcdoc = `<style>
+ * { color:${neither}; }
+ :defined { color:${defined}; }
+ :not(:defined) { color:${not_defined}; }
+</style>`
+ + testList.map(d => `<${d.tag_name}></${d.tag_name}>`).join('');
+setup({ explicit_done: true });
+iframe.onload = () => {
+ const doc = iframe.contentDocument;
+ const doc_without_browsing_context = doc.implementation.createHTMLDocument();
+ for (const data of testList) {
+ // Test elements inserted by parser.
+ test_defined(data.defined, doc.getElementsByTagName(data.tag_name)[0], `<${data.tag_name}>`);
+
+ // Test DOM createElement() methods.
+ let try_upgrade = !data.defined;
+ test_defined_for_createElement(data.defined, try_upgrade, doc, data.tag_name);
+
+ // Documents without browsing context should behave the same.
+ test_defined_for_createElement(data.defined, false, doc_without_browsing_context, data.tag_name, data.is, 'Without browsing context: ');
+ }
+
+ done();
+};
+
+function test_defined_for_createElement(defined, should_test_change, doc, tag_name, is, description = '') {
+ // Test document.createElement().
+ let element = doc.createElement(tag_name);
+ doc.body.appendChild(element);
+ test_defined(defined, element, `${description}createElement("${tag_name}")`);
+
+ // Test document.createElementNS().
+ let html_element = doc.createElementNS('http://www.w3.org/1999/xhtml', tag_name);
+ doc.body.appendChild(html_element);
+ test_defined(defined, html_element, `${description}createElementNS("http://www.w3.org/1999/xhtml", "${tag_name}")`);
+
+ // If the element namespace is not HTML, it should be "uncustomized"; i.e., "defined".
+ let svg_element = doc.createElementNS('http://www.w3.org/2000/svg', tag_name);
+ doc.body.appendChild(svg_element);
+ test_defined(true, svg_element, `${description}createElementNS("http://www.w3.org/2000/svg", "${tag_name}")`);
+
+ // Test ":defined" changes when the custom element was defined.
+ if (should_test_change) {
+ let w = doc.defaultView;
+ assert_false(!w, 'defaultView required to test change');
+ w.customElements.define(tag_name, class extends w.HTMLElement {
+ constructor() { super(); }
+ });
+ test_defined(true, element, `Upgraded ${description}createElement("${tag_name}")`);
+ test_defined(true, html_element, `Upgraded ${description}createElementNS("http://www.w3.org/1999/xhtml", "${tag_name}")`);
+ }
+}
+
+function test_defined(expected, element, description) {
+ test(() => {
+ assert_equals(element.matches(':defined'), expected, 'matches(":defined")');
+ assert_equals(element.matches(':not(:defined)'), !expected, 'matches(":not(:defined")');
+ const view = element.ownerDocument.defaultView;
+ if (!view)
+ return;
+ const style = view.getComputedStyle(element);
+ assert_equals(style.color, expected ? defined : not_defined, 'getComputedStyle');
+ }, `${description} should ${expected ? 'be' : 'not be'} :defined`);
+}
+
+test(function () {
+ var log = [];
+ var instance = document.createElement('my-custom-element-2');
+ document.body.appendChild(instance);
+ assert_false(instance.matches(":defined"), "Prior to definition, instance should not match :defined");
+ customElements.define('my-custom-element-2',class extends HTMLElement {
+ constructor() {
+ assert_false(instance.matches(":defined"), "During construction, prior to super(), instance should not match :defined");
+ super();
+ log.push([this, 'begin']);
+ assert_false(this.matches(":defined"), "During construction, after super(), this should not match :defined");
+ log.push([this, 'end']);
+ }
+ });
+ assert_true(instance.matches(":defined"), "After construction, instance should match :defined");
+ assert_equals(log.length, 2);
+ assert_array_equals(log[0], [instance, 'begin']);
+ assert_array_equals(log[1], [instance, 'end']);
+}, 'this.matches(:defined) should not match during an upgrade');
+
+</script>
diff --git a/testing/web-platform/tests/custom-elements/range-and-constructors.html b/testing/web-platform/tests/custom-elements/range-and-constructors.html
new file mode 100644
index 0000000000..cc51424851
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/range-and-constructors.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom elements: Range APIs should invoke constructor in tree order</title>
+<meta name="author" title="Edgar Chen" href="mailto:echen@mozilla.com">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/scripting.html#concept-upgrade-an-element">
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-create-element">
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-range-extract">
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-range-clone">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+
+<c-e id="root">
+ <c-e id="root-0">
+ <c-e id="root-0-0">
+ <c-e id="root-0-0-0"></c-e>
+ <span id="start"></span>
+ </c-e>
+ </c-e>
+ <c-e id="root-1"></c-e>
+ <span id="end"></span>
+</c-e>
+
+<script>
+
+var logs = [];
+class CE extends HTMLElement {
+ constructor() {
+ super();
+ logs.push(this.id);
+ }
+}
+customElements.define('c-e', CE);
+
+function getRange() {
+ const range = new Range();
+ range.setStart(document.getElementById('start'), 0);
+ range.setEnd(document.getElementById('end'), 0);
+ return range;
+}
+
+test(function () {
+ // Clear log for testing.
+ logs = [];
+ getRange().cloneContents();
+ assert_array_equals(logs, ['root-0', 'root-0-0', 'root-1']);
+}, 'Range.cloneContents should invoke constructor in tree order');
+
+test(function () {
+ // Clear log for testing.
+ logs = [];
+ getRange().extractContents();
+ assert_array_equals(logs, ['root-0', 'root-0-0']);
+}, 'Range.extractContents should invoke constructor in tree order');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/reaction-timing.html b/testing/web-platform/tests/custom-elements/reaction-timing.html
new file mode 100644
index 0000000000..454cc26c0f
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reaction-timing.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: Custom element reactions must be invoked before returning to author scripts</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="Custom element reactions must be invoked before returning to author scripts">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/scripting.html#invoke-custom-element-reactions">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+class MyCustomElement extends HTMLElement {
+ attributeChangedCallback(...args) {
+ this.handler(...args);
+ }
+
+ handler() { }
+}
+MyCustomElement.observedAttributes = ['data-title', 'title'];
+customElements.define('my-custom-element', MyCustomElement);
+
+test(function () {
+ var instance = document.createElement('my-custom-element');
+ var anotherInstance = document.createElement('my-custom-element');
+
+ var callbackOrder = [];
+ instance.handler = function () {
+ callbackOrder.push([this, 'begin']);
+ anotherInstance.setAttribute('data-title', 'baz');
+ callbackOrder.push([this, 'end']);
+ }
+ anotherInstance.handler = function () {
+ callbackOrder.push([this, 'begin']);
+ callbackOrder.push([this, 'end']);
+ }
+
+ instance.setAttribute('title', 'foo');
+ assert_equals(callbackOrder.length, 4);
+
+ assert_array_equals(callbackOrder[0], [instance, 'begin']);
+ assert_array_equals(callbackOrder[1], [anotherInstance, 'begin']);
+ assert_array_equals(callbackOrder[2], [anotherInstance, 'end']);
+ assert_array_equals(callbackOrder[3], [instance, 'end']);
+
+}, 'setAttribute and removeAttribute must enqueue and invoke attributeChangedCallback');
+
+test(function () {
+ var instance = document.createElement('my-custom-element');
+ var anotherInstance = document.createElement('my-custom-element');
+
+ var callbackOrder = [];
+ instance.handler = function () {
+ callbackOrder.push([this, 'begin']);
+ anotherInstance.toggleAttribute('data-title');
+ callbackOrder.push([this, 'end']);
+ }
+ anotherInstance.handler = function () {
+ callbackOrder.push([this, 'begin']);
+ callbackOrder.push([this, 'end']);
+ }
+
+ instance.toggleAttribute('title');
+ assert_equals(callbackOrder.length, 4);
+
+ assert_array_equals(callbackOrder[0], [instance, 'begin']);
+ assert_array_equals(callbackOrder[1], [anotherInstance, 'begin']);
+ assert_array_equals(callbackOrder[2], [anotherInstance, 'end']);
+ assert_array_equals(callbackOrder[3], [instance, 'end']);
+
+}, 'toggleAttribute must enqueue and invoke attributeChangedCallback');
+
+test(function () {
+ var shouldCloneAnotherInstance = false;
+ var anotherInstanceClone;
+ var log = [];
+
+ class SelfCloningElement extends HTMLElement {
+ constructor() {
+ super();
+ log.push([this, 'begin']);
+ if (shouldCloneAnotherInstance) {
+ shouldCloneAnotherInstance = false;
+ anotherInstanceClone = anotherInstance.cloneNode(false);
+ }
+ log.push([this, 'end']);
+ }
+ }
+ customElements.define('self-cloning-element', SelfCloningElement);
+
+ var instance = document.createElement('self-cloning-element');
+ var anotherInstance = document.createElement('self-cloning-element');
+ shouldCloneAnotherInstance = true;
+
+ assert_equals(log.length, 4);
+ var instanceClone = instance.cloneNode(false);
+
+ assert_equals(log.length, 8);
+ assert_array_equals(log[0], [instance, 'begin']);
+ assert_array_equals(log[1], [instance, 'end']);
+ assert_array_equals(log[2], [anotherInstance, 'begin']);
+ assert_array_equals(log[3], [anotherInstance, 'end']);
+ assert_array_equals(log[4], [instanceClone, 'begin']);
+ assert_array_equals(log[5], [anotherInstanceClone, 'begin']);
+ assert_array_equals(log[6], [anotherInstanceClone, 'end']);
+ assert_array_equals(log[7], [instanceClone, 'end']);
+}, 'Calling Node.prototype.cloneNode(false) must push a new element queue to the processing stack');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/reactions/Animation.html b/testing/web-platform/tests/custom-elements/reactions/Animation.html
new file mode 100644
index 0000000000..f8d3bb86ed
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/Animation.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: CEReactions on Element interface</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="commitStyles of Animation interface must have CEReactions">
+<meta name="help" content="https://dom.spec.whatwg.org/#element">
+<meta name="help" content="https://w3c.github.io/DOM-Parsing/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<script src="./resources/reactions.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+test(function () {
+ const element = define_new_custom_element(['style']);
+ const instance = document.createElement(element.name);
+ document.body.appendChild(instance);
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+
+ const animation = instance.animate([{'borderColor': 'rgb(0, 0, 255)'}], 1);
+ animation.commitStyles();
+
+ const logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_equals(logEntries.last().name, 'style');
+ assert_equals(logEntries.last().namespace, null);
+}, 'Animation.animate must enqueue an attributeChanged reaction when it adds the observed style attribute');
+
+test(function () {
+ const element = define_new_custom_element(['style']);
+ const instance = document.createElement(element.name);
+ document.body.appendChild(instance);
+
+ let animation = instance.animate([{'borderColor': 'rgb(0, 0, 255)'}], 1);
+ animation.commitStyles();
+
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected', 'attributeChanged']);
+
+ animation = instance.animate([{'borderColor': 'rgb(0, 255, 0)'}]);
+ animation.commitStyles();
+
+ const logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_equals(logEntries.last().name, 'style');
+ assert_equals(logEntries.last().namespace, null);
+}, 'Animation.animate must enqueue an attributeChanged reaction when it mutates the observed style attribute');
+
+test(function () {
+ const element = define_new_custom_element([]);
+ const instance = document.createElement(element.name);
+ document.body.appendChild(instance);
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+
+ const animation = instance.animate([{'borderColor': 'rgb(0, 0, 255)'}], 1);
+ animation.commitStyles();
+
+ assert_array_equals(element.takeLog().types(), []);
+}, 'Animation.animate must not enqueue an attributeChanged reaction when it mutates the style attribute but the style attribute is not observed');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/reactions/AriaMixin-element-attributes.html b/testing/web-platform/tests/custom-elements/reactions/AriaMixin-element-attributes.html
new file mode 100644
index 0000000000..eec6dee03b
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/AriaMixin-element-attributes.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: CEReactions on Element interface</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="Element attributes of AriaAttributes interface must have CEReactions">
+<meta name="help" content="https://dom.spec.whatwg.org/#element">
+<meta name="help" content="https://w3c.github.io/DOM-Parsing/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<script src="./resources/reactions.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<div id="parentElement"></div>
+<script>
+
+function testElementReflectAttribute(jsAttributeName, contentAttributeName, validValue1, validValue2, name, getParentElement) {
+ test(function () {
+ let element = define_new_custom_element([contentAttributeName]);
+ let instance = document.createElement(element.name);
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ parentElement.appendChild(instance);
+ assert_array_equals(element.takeLog().types(), ['connected']);
+ instance[jsAttributeName] = validValue1;
+ let logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+
+ assert_attribute_log_entry(logEntries.last(), {name: contentAttributeName, oldValue: null, newValue: "", namespace: null});
+ }, name + ' must enqueue an attributeChanged reaction when adding ' + contentAttributeName + ' content attribute');
+
+ test(function () {
+ let element = define_new_custom_element([contentAttributeName]);
+ let instance = document.createElement(element.name);
+ parentElement.appendChild(instance);
+ instance[jsAttributeName] = validValue1;
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected', 'attributeChanged']);
+ instance[jsAttributeName] = validValue2;
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: contentAttributeName, oldValue: "", newValue: "", namespace: null});
+ }, name + ' must enqueue an attributeChanged reaction when replacing an existing attribute');
+}
+
+const dummy1 = document.createElement('div');
+dummy1.id = 'dummy1';
+document.body.appendChild(dummy1);
+
+const dummy2 = document.createElement('div');
+dummy2.id = 'dummy2';
+document.body.appendChild(dummy2);
+
+testElementReflectAttribute('ariaActiveDescendantElement', 'aria-activedescendant', dummy1, dummy2, 'ariaActiveDescendantElement in Element');
+testElementReflectAttribute('ariaControlsElements', 'aria-controls', [dummy1], [dummy2], 'ariaControlsElements in Element');
+testElementReflectAttribute('ariaDescribedByElements', 'aria-describedby', [dummy1], [dummy2], 'ariaDescribedByElements in Element');
+testElementReflectAttribute('ariaDetailsElements', 'aria-details', [dummy1], [dummy2], 'ariaDetailsElements in Element');
+testElementReflectAttribute('ariaErrorMessageElements', 'aria-errormessage', [dummy1], [dummy2], 'ariaErrorMessageElements in Element');
+testElementReflectAttribute('ariaFlowToElements', 'aria-flowto', [dummy1], [dummy2], 'ariaFlowToElements in Element');
+testElementReflectAttribute('ariaLabelledByElements', 'aria-labelledby', [dummy1], [dummy2], 'ariaLabelledByElements in Element')
+testElementReflectAttribute('ariaOwnsElements', 'aria-owns', [dummy1], [dummy2], 'ariaOwnsElements in Element')
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/reactions/AriaMixin-string-attributes.html b/testing/web-platform/tests/custom-elements/reactions/AriaMixin-string-attributes.html
new file mode 100644
index 0000000000..f71bf2daa9
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/AriaMixin-string-attributes.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: CEReactions on Element interface</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="String attributes of AriaAttributes interface must have CEReactions">
+<meta name="help" content="https://dom.spec.whatwg.org/#element">
+<meta name="help" content="https://w3c.github.io/DOM-Parsing/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<script src="./resources/reactions.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<div id="container"></div>
+<script>
+
+testReflectAttribute('ariaAtomic', 'aria-atomic', 'foo', 'bar', 'ariaAtomic on Element');
+testReflectAttribute('ariaAutoComplete', 'aria-autocomplete', 'foo', 'bar', 'ariaAutoComplete on Element');
+testReflectAttribute('ariaBusy', 'aria-busy', 'foo', 'bar', 'ariaBusy on Element');
+testReflectAttribute('ariaChecked', 'aria-checked', 'foo', 'bar', 'ariaChecked on Element');
+testReflectAttribute('ariaColCount', 'aria-colcount', 'foo', 'bar', 'ariaColCount on Element');
+testReflectAttribute('ariaColIndex', 'aria-colindex', 'foo', 'bar', 'ariaColIndex on Element');
+testReflectAttribute('ariaColSpan', 'aria-colspan', 'foo', 'bar', 'ariaColSpan on Element');
+
+testReflectAttribute('ariaCurrent', 'aria-current', 'foo', 'bar', 'ariaCurrent on Element');
+
+testReflectAttribute('ariaDisabled', 'aria-disabled', 'foo', 'bar', 'ariaDisabled on Element');
+
+testReflectAttribute('ariaExpanded', 'aria-expanded', 'foo', 'bar', 'ariaExpanded on Element');
+
+testReflectAttribute('ariaHasPopup', 'aria-haspopup', 'foo', 'bar', 'ariaHasPopup on Element');
+testReflectAttribute('ariaHidden', 'aria-hidden', 'foo', 'bar', 'ariaHidden on Element');
+testReflectAttribute('ariaInvalid', 'aria-invalid', 'foo', 'bar', 'ariaInvalid on Element');
+testReflectAttribute('ariaKeyShortcuts', 'aria-keyshortcuts', 'foo', 'bar', 'ariaKeyShortcuts on Element');
+testReflectAttribute('ariaLabel', 'aria-label', 'foo', 'bar', 'ariaLabel on Element');
+
+testReflectAttribute('ariaLevel', 'aria-level', 'foo', 'bar', 'ariaLevel on Element');
+testReflectAttribute('ariaLive', 'aria-live', 'foo', 'bar', 'ariaLive on Element');
+testReflectAttribute('ariaModal', 'aria-modal', 'foo', 'bar', 'ariaModal on Element');
+testReflectAttribute('ariaMultiLine', 'aria-multiline', 'foo', 'bar', 'ariaMultiLine on Element');
+testReflectAttribute('ariaMultiSelectable', 'aria-multiselectable', 'foo', 'bar', 'ariaMultiSelectable on Element');
+testReflectAttribute('ariaOrientation', 'aria-orientation', 'foo', 'bar', 'ariaOrientation on Element');
+
+testReflectAttribute('ariaPlaceholder', 'aria-placeholder', 'foo', 'bar', 'ariaPlaceholder on Element');
+testReflectAttribute('ariaPosInSet', 'aria-posinset', 'foo', 'bar', 'ariaPosInSet on Element');
+testReflectAttribute('ariaPressed', 'aria-pressed', 'foo', 'bar', 'ariaPressed on Element');
+testReflectAttribute('ariaReadOnly', 'aria-readonly', 'foo', 'bar', 'ariaReadOnly on Element');
+testReflectAttribute('ariaRelevant', 'aria-relevant', 'foo', 'bar', 'ariaRelevant on Element');
+testReflectAttribute('ariaRequired', 'aria-required', 'foo', 'bar', 'ariaRequired on Element');
+testReflectAttribute('ariaRoleDescription', 'aria-roledescription', 'foo', 'bar', 'ariaRoleDescription on Element');
+testReflectAttribute('ariaRowCount', 'aria-rowcount', 'foo', 'bar', 'ariaRowCount on Element');
+testReflectAttribute('ariaRowIndex', 'aria-rowindex', 'foo', 'bar', 'ariaRowIndex on Element');
+testReflectAttribute('ariaRowSpan', 'aria-rowspan', 'foo', 'bar', 'ariaRowSpan on Element');
+testReflectAttribute('ariaSelected', 'aria-selected', 'foo', 'bar', 'ariaSelected on Element');
+testReflectAttribute('ariaSetSize', 'aria-setsize', 'foo', 'bar', 'ariaSetSize on Element');
+testReflectAttribute('ariaSort', 'aria-sort', 'foo', 'bar', 'ariaSort on Element');
+testReflectAttribute('ariaValueMax', 'aria-valuemax', 'foo', 'bar', 'ariaValueMax on Element');
+testReflectAttribute('ariaValueMin', 'aria-valuemin', 'foo', 'bar', 'ariaValueMin on Element');
+testReflectAttribute('ariaValueNow', 'aria-valuenow', 'foo', 'bar', 'ariaValueNow on Element');
+testReflectAttribute('ariaValueText', 'aria-valuetext', 'foo', 'bar', 'ariaValueText on Element');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/reactions/Attr.html b/testing/web-platform/tests/custom-elements/reactions/Attr.html
new file mode 100644
index 0000000000..c9fa37f961
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/Attr.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: CEReactions on Attr interface</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="value of Attr interface must have CEReactions">
+<meta name="help" content="https://dom.spec.whatwg.org/#node">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<script src="./resources/reactions.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+testAttributeMutator(function (element, name, value) {
+ element.attributes[name].value = value;
+}, 'value on Attr');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/reactions/CSSStyleDeclaration.html b/testing/web-platform/tests/custom-elements/reactions/CSSStyleDeclaration.html
new file mode 100644
index 0000000000..95274d8c75
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/CSSStyleDeclaration.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: CEReactions on CSSStyleDeclaration interface</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="cssText, setProperty, setPropertyValue, setPropertyPriority, removeProperty, cssFloat, and all camel cased attributes of CSSStyleDeclaration interface must have CEReactions">
+<meta name="help" content="https://dom.spec.whatwg.org/#node">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<script src="./resources/reactions.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+test_mutating_style_property_value(function (instance, propertyName, idlName, value) {
+ instance.style.cssText = `${propertyName}: ${value}`;
+}, 'cssText on CSSStyleDeclaration');
+
+test_mutating_style_property_value(function (instance, propertyName, idlName, value) {
+ instance.style.setProperty(propertyName, value);
+}, 'setProperty on CSSStyleDeclaration');
+
+test_mutating_style_property_priority(function (instance, propertyName, idlName, isImportant) {
+ instance.style.setProperty(propertyName, instance.style[idlName], isImportant ? 'important': '');
+}, 'setProperty on CSSStyleDeclaration');
+
+if (CSSStyleDeclaration.prototype.setPropertyValue) {
+ test_mutating_style_property_value(function (instance, propertyName, idlName, value) {
+ instance.style.setPropertyValue(propertyName, value);
+ }, 'setPropertyValue on CSSStyleDeclaration');
+}
+
+if (CSSStyleDeclaration.prototype.setPropertyPriority) {
+ test_mutating_style_property_priority(function (instance, propertyName, idlName, isImportant) {
+ instance.style.setPropertyPriority(propertyName, isImportant ? 'important': '');
+ }, 'setPropertyPriority on CSSStyleDeclaration');
+}
+
+test_removing_style_property_value(function (instance, propertyName, idlName) {
+ instance.style.removeProperty(propertyName);
+}, 'removeProperty on CSSStyleDeclaration');
+
+test(function () {
+ var element = define_new_custom_element(['style']);
+ var instance = document.createElement(element.name);
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ instance.style.cssFloat = 'left';
+ assert_equals(instance.getAttribute('style'), 'float: left;');
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'style', oldValue: null, newValue: 'float: left;', namespace: null});
+}, 'cssFloat on CSSStyleDeclaration must enqueue an attributeChanged reaction when it adds the observed style attribute');
+
+test(function () {
+ var element = define_new_custom_element([]);
+ var instance = document.createElement(element.name);
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ instance.style.cssFloat = 'left';
+ assert_equals(instance.getAttribute('style'), 'float: left;');
+ assert_array_equals(element.takeLog().types(), []);
+}, 'cssFloat on CSSStyleDeclaration must not enqueue an attributeChanged reaction when it adds the style attribute but the style attribute is not observed');
+
+test_mutating_style_property_value(function (instance, propertyName, idlName, value) {
+ assert_equals(idlName, 'borderWidth');
+ instance.style.borderWidth = value;
+}, 'A camel case attribute (borderWidth) on CSSStyleDeclaration',
+ {propertyName: 'border-width', idlName: 'borderWidth', value1: '2px', value2: '4px'});
+
+test_mutating_style_property_value(function (instance, propertyName, idlName, value) {
+ assert_equals(propertyName, 'border-width');
+ instance.style['border-width'] = value;
+}, 'A dashed property (border-width) on CSSStyleDeclaration',
+ {propertyName: 'border-width', idlName: 'borderWidth', value1: '1px', value2: '5px'});
+
+test_mutating_style_property_value(function (instance, propertyName, idlName, value) {
+ instance.style.webkitFilter = value;
+}, 'A webkit prefixed camel case attribute (webkitFilter) on CSSStyleDeclaration',
+ {propertyName: 'filter', idlName: 'filter', value1: 'grayscale(20%)', value2: 'grayscale(30%)'});
+
+test_mutating_style_property_value(function (instance, propertyName, idlName, value) {
+ instance.style['-webkit-filter'] = value;
+}, 'A webkit prefixed dashed property (-webkit-filter) on CSSStyleDeclaration',
+ {propertyName: 'filter', idlName: 'filter', value1: 'grayscale(20%)', value2: 'grayscale(30%)'});
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/reactions/ChildNode.html b/testing/web-platform/tests/custom-elements/reactions/ChildNode.html
new file mode 100644
index 0000000000..f808b67a80
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/ChildNode.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: CEReactions on ChildNode interface</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="before, after, replaceWith, and remove of ChildNode interface must have CEReactions">
+<meta name="help" content="https://dom.spec.whatwg.org/#childnode">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<script src="./resources/reactions.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+testNodeConnector(function (newContainer, customElement) {
+ newContainer.firstChild.before(customElement);
+}, 'before on ChildNode');
+
+testNodeConnector(function (newContainer, customElement) {
+ newContainer.firstChild.after(customElement);
+}, 'after on ChildNode');
+
+testNodeConnector(function (newContainer, customElement) {
+ newContainer.firstChild.replaceWith(customElement);
+}, 'replaceWith on ChildNode');
+
+testNodeDisconnector(function (customElement) {
+ customElement.remove();
+}, 'remove on ChildNode');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/reactions/DOMStringMap.html b/testing/web-platform/tests/custom-elements/reactions/DOMStringMap.html
new file mode 100644
index 0000000000..5e34dfe2ba
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/DOMStringMap.html
@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: CEReactions on DOMStringMap interface</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="setter and deleter of DOMStringMap interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#domstringmap">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<script src="./resources/reactions.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+test(function () {
+ var element = define_new_custom_element(['data-foo']);
+ var instance = document.createElement(element.name);
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ instance.dataset.foo = 'bar';
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'data-foo', oldValue: null, newValue: 'bar', namespace: null});
+}, 'setter on DOMStringMap must enqueue an attributeChanged reaction when adding an observed data attribute');
+
+test(function () {
+ var element = define_new_custom_element(['data-bar']);
+ var instance = document.createElement(element.name);
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ instance.dataset.foo = 'bar';
+ assert_array_equals(element.takeLog().types(), []);
+}, 'setter on DOMStringMap must not enqueue an attributeChanged reaction when adding an unobserved data attribute');
+
+test(function () {
+ var element = define_new_custom_element(['data-foo']);
+ var instance = document.createElement(element.name);
+ instance.dataset.foo = 'bar';
+ assert_array_equals(element.takeLog().types(), ['constructed', 'attributeChanged']);
+ instance.dataset.foo = 'baz';
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'data-foo', oldValue: 'bar', newValue: 'baz', namespace: null});
+}, 'setter on DOMStringMap must enqueue an attributeChanged reaction when mutating the value of an observed data attribute');
+
+test(function () {
+ var element = define_new_custom_element(['data-foo']);
+ var instance = document.createElement(element.name);
+ instance.dataset.foo = 'bar';
+ assert_array_equals(element.takeLog().types(), ['constructed', 'attributeChanged']);
+ instance.dataset.foo = 'bar';
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'data-foo', oldValue: 'bar', newValue: 'bar', namespace: null});
+}, 'setter on DOMStringMap must enqueue an attributeChanged reaction when mutating the value of an observed data attribute to the same value');
+
+test(function () {
+ var element = define_new_custom_element(['data-zero']);
+ var instance = document.createElement(element.name);
+ instance.dataset.foo = 'bar';
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ instance.dataset.foo = 'baz';
+ assert_array_equals(element.takeLog().types(), []);
+}, 'setter on DOMStringMap must not enqueue an attributeChanged reaction when mutating the value of an unobserved data attribute');
+
+test(function () {
+ var element = define_new_custom_element(['data-foo']);
+ var instance = document.createElement(element.name);
+ instance.dataset.foo = 'bar';
+ assert_array_equals(element.takeLog().types(), ['constructed', 'attributeChanged']);
+ delete instance.dataset.foo;
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'data-foo', oldValue: 'bar', newValue: null, namespace: null});
+}, 'deleter on DOMStringMap must enqueue an attributeChanged reaction when removing an observed data attribute');
+
+test(function () {
+ var element = define_new_custom_element(['data-bar']);
+ var instance = document.createElement(element.name);
+ instance.dataset.foo = 'bar';
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ delete instance.dataset.foo;
+ assert_array_equals(element.takeLog().types(), []);
+}, 'deleter on DOMStringMap must not enqueue an attributeChanged reaction when removing an unobserved data attribute');
+
+test(function () {
+ var element = define_new_custom_element(['data-foo']);
+ var instance = document.createElement(element.name);
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ delete instance.dataset.foo;
+ assert_array_equals(element.takeLog().types(), []);
+}, 'deleter on DOMStringMap must not enqueue an attributeChanged reaction when it does not remove a data attribute');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/reactions/DOMTokenList.html b/testing/web-platform/tests/custom-elements/reactions/DOMTokenList.html
new file mode 100644
index 0000000000..14a643c4a8
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/DOMTokenList.html
@@ -0,0 +1,210 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: CEReactions on DOMTokenList interface</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="add, remove, toggle, replace, and the stringifier of DOMTokenList interface must have CEReactions">
+<meta name="help" content="https://dom.spec.whatwg.org/#node">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<script src="./resources/reactions.js"></script>
+</head>
+<body>
+<script>
+
+test(function () {
+ var element = define_new_custom_element(['class']);
+ var instance = document.createElement(element.name);
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ instance.classList.add('foo');
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'class', oldValue: null, newValue: 'foo', namespace: null});
+}, 'add on DOMTokenList must enqueue an attributeChanged reaction when adding an attribute');
+
+test(function () {
+ var element = define_new_custom_element(['style']);
+ var instance = document.createElement(element.name);
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ instance.classList.add('foo');
+ assert_array_equals(element.takeLog().types(), []);
+}, 'add on DOMTokenList must not enqueue an attributeChanged reaction when adding an unobserved attribute');
+
+test(function () {
+ var element = define_new_custom_element(['class']);
+ var instance = document.createElement(element.name);
+ instance.setAttribute('class', 'hello');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'attributeChanged']);
+ instance.classList.add('world');
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'class', oldValue: 'hello', newValue: 'hello world', namespace: null});
+}, 'add on DOMTokenList must enqueue an attributeChanged reaction when adding a value to an existing attribute');
+
+test(function () {
+ var element = define_new_custom_element(['contenteditable']);
+ var instance = document.createElement(element.name);
+ instance.setAttribute('class', 'hello');
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ instance.classList.add('world');
+ assert_array_equals(element.takeLog().types(), []);
+}, 'add on DOMTokenList must not enqueue an attributeChanged reaction when adding a value to an unobserved attribute');
+
+test(function () {
+ var element = define_new_custom_element(['class']);
+ var instance = document.createElement(element.name);
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ instance.classList.add('hello', 'world');
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'class', oldValue: null, newValue: 'hello world', namespace: null});
+}, 'add on DOMTokenList must enqueue exactly one attributeChanged reaction when adding multiple values to an attribute');
+
+test(function () {
+ var element = define_new_custom_element(['class']);
+ var instance = document.createElement(element.name);
+ instance.setAttribute('class', 'hello world');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'attributeChanged']);
+ instance.classList.remove('world');
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'class', oldValue: 'hello world', newValue: 'hello', namespace: null});
+}, 'remove on DOMTokenList must enqueue an attributeChanged reaction when removing a value from an attribute');
+
+test(function () {
+ var element = define_new_custom_element(['class']);
+ var instance = document.createElement(element.name);
+ instance.setAttribute('class', 'hello foo world bar');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'attributeChanged']);
+ instance.classList.remove('hello', 'world');
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'class', oldValue: 'hello foo world bar', newValue: 'foo bar', namespace: null});
+}, 'remove on DOMTokenList must enqueue exactly one attributeChanged reaction when removing multiple values to an attribute');
+
+test(function () {
+ var element = define_new_custom_element(['class']);
+ var instance = document.createElement(element.name);
+ instance.setAttribute('class', 'hello world');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'attributeChanged']);
+ instance.classList.remove('foo');
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'class', oldValue: 'hello world', newValue: 'hello world', namespace: null});
+}, 'remove on DOMTokenList must enqueue an attributeChanged reaction even when removing a non-existent value from an attribute');
+
+test(function () {
+ var element = define_new_custom_element(['title']);
+ var instance = document.createElement(element.name);
+ instance.setAttribute('class', 'hello world');
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ instance.classList.remove('world');
+ assert_array_equals(element.takeLog().types(), []);
+}, 'remove on DOMTokenList must not enqueue an attributeChanged reaction when removing a value from an unobserved attribute');
+
+test(function () {
+ var element = define_new_custom_element(['class']);
+ var instance = document.createElement(element.name);
+ instance.setAttribute('class', 'hello');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'attributeChanged']);
+ instance.classList.toggle('world');
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'class', oldValue: 'hello', newValue: 'hello world', namespace: null});
+}, 'toggle on DOMTokenList must enqueue an attributeChanged reaction when adding a value to an attribute');
+
+test(function () {
+ var element = define_new_custom_element(['class']);
+ var instance = document.createElement(element.name);
+ instance.setAttribute('class', 'hello world');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'attributeChanged']);
+ instance.classList.toggle('world');
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'class', oldValue: 'hello world', newValue: 'hello', namespace: null});
+}, 'toggle on DOMTokenList must enqueue an attributeChanged reaction when removing a value from an attribute');
+
+test(function () {
+ var element = define_new_custom_element(['class']);
+ var instance = document.createElement(element.name);
+ instance.setAttribute('class', 'hello');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'attributeChanged']);
+ instance.classList.replace('hello', 'world');
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'class', oldValue: 'hello', newValue: 'world', namespace: null});
+}, 'replace on DOMTokenList must enqueue an attributeChanged reaction when replacing a value in an attribute');
+
+test(function () {
+ var element = define_new_custom_element(['class']);
+ var instance = document.createElement(element.name);
+ instance.setAttribute('class', 'hello world');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'attributeChanged']);
+ instance.classList.replace('foo', 'bar');
+ assert_array_equals(element.takeLog().types(), []);
+}, 'replace on DOMTokenList must not enqueue an attributeChanged reaction when the token to replace does not exist in the attribute');
+
+test(function () {
+ var element = define_new_custom_element(['title']);
+ var instance = document.createElement(element.name);
+ instance.setAttribute('class', 'hello');
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ instance.classList.replace('hello', 'world');
+ assert_array_equals(element.takeLog().types(), []);
+}, 'replace on DOMTokenList must not enqueue an attributeChanged reaction when replacing a value in an unobserved attribute');
+
+test(function () {
+ var element = define_new_custom_element(['class']);
+ var instance = document.createElement(element.name);
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ instance.classList = 'hello';
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'class', oldValue: null, newValue: 'hello', namespace: null});
+}, 'the stringifier of DOMTokenList must enqueue an attributeChanged reaction when adding an observed attribute');
+
+test(function () {
+ var element = define_new_custom_element(['id']);
+ var instance = document.createElement(element.name);
+ instance.setAttribute('class', 'hello');
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ instance.classList = 'hello';
+ var logEntries = element.takeLog();
+ assert_array_equals(element.takeLog().types(), []);
+}, 'the stringifier of DOMTokenList must not enqueue an attributeChanged reaction when adding an unobserved attribute');
+
+test(function () {
+ var element = define_new_custom_element(['class']);
+ var instance = document.createElement(element.name);
+ instance.setAttribute('class', 'hello');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'attributeChanged']);
+ instance.classList = 'world';
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'class', oldValue: 'hello', newValue: 'world', namespace: null});
+}, 'the stringifier of DOMTokenList must enqueue an attributeChanged reaction when mutating the value of an observed attribute');
+
+test(function () {
+ var element = define_new_custom_element([]);
+ var instance = document.createElement(element.name);
+ instance.setAttribute('class', 'hello');
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ instance.classList = 'world';
+ assert_array_equals(element.takeLog().types(), []);
+}, 'the stringifier of DOMTokenList must not enqueue an attributeChanged reaction when mutating the value of an unobserved attribute');
+
+test(function () {
+ var element = define_new_custom_element(['class']);
+ var instance = document.createElement(element.name);
+ instance.setAttribute('class', 'hello');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'attributeChanged']);
+ instance.classList = 'hello';
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'class', oldValue: 'hello', newValue: 'hello', namespace: null});
+}, 'the stringifier of DOMTokenList must enqueue an attributeChanged reaction when the setter is called with the original value of the attribute');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/reactions/Document.html b/testing/web-platform/tests/custom-elements/reactions/Document.html
new file mode 100644
index 0000000000..1f05982a90
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/Document.html
@@ -0,0 +1,156 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: CEReactions on Document interface</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="importNode and adoptNode of Document interface must have CEReactions">
+<meta name="help" content="https://dom.spec.whatwg.org/#document">
+<meta name="help" content="https://html.spec.whatwg.org/#document">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<script src="./resources/reactions.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ const instance = contentDocument.createElement('custom-element');
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+
+ const newDoc = contentDocument.implementation.createHTMLDocument();
+ newDoc.importNode(instance);
+
+ assert_array_equals(element.takeLog().types(), []);
+}, 'importNode on Document must not construct a new custom element when importing a custom element into a window-less document');
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ const template = contentDocument.createElement('template');
+ template.innerHTML = '<custom-element></custom-element>';
+ assert_array_equals(element.takeLog().types(), []);
+ contentDocument.importNode(template.content, true);
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+}, 'importNode on Document must construct a new custom element when importing a custom element from a template');
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ const instance = contentDocument.createElement('custom-element');
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+
+ const newDoc = contentDocument.implementation.createHTMLDocument();
+ newDoc.adoptNode(instance);
+
+ const logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['adopted']);
+ assert_equals(logEntries.last().oldDocument, contentDocument);
+ assert_equals(logEntries.last().newDocument, newDoc);
+}, 'adoptNode on Document must enqueue an adopted reaction when importing a custom element');
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ const instance = contentDocument.createElement('custom-element');
+
+ const container = contentDocument.createElement('div');
+ container.contentEditable = true;
+ container.appendChild(instance);
+ contentDocument.body.appendChild(container);
+
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+
+ container.focus();
+ contentDocument.execCommand('delete', false, null);
+
+ assert_array_equals(element.takeLog().types(), ['disconnected']);
+}, 'execCommand on Document must enqueue a disconnected reaction when deleting a custom element from a contenteditable element');
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+
+ contentDocument.title = '';
+ const title = contentDocument.querySelector('title');
+ const instance = contentDocument.createElement('custom-element');
+ title.appendChild(instance);
+ instance.textContent = 'hello';
+
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+ assert_equals(title.innerHTML, '<custom-element>hello</custom-element>');
+
+ title.text = 'world';
+ assert_equals(title.innerHTML, 'world');
+ assert_array_equals(element.takeLog().types(), ['disconnected']);
+}, 'title on Document must enqueue disconnectedCallback when removing a custom element');
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+
+ const body = contentDocument.body;
+ body.innerHTML = '<custom-element>hello</custom-element>';
+
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+ assert_equals(body.innerHTML, '<custom-element>hello</custom-element>');
+
+ contentDocument.body = contentDocument.createElement('body');
+ assert_array_equals(element.takeLog().types(), ['disconnected']);
+}, 'body on Document must enqueue disconnectedCallback when removing a custom element');
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+
+ const instance = contentDocument.createElement('custom-element');
+ const body = contentDocument.createElement('body');
+ body.appendChild(instance);
+
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ assert_equals(body.innerHTML, '<custom-element></custom-element>');
+
+ contentDocument.body = body;
+ assert_array_equals(element.takeLog().types(), ['connected']);
+}, 'body on Document must enqueue connectedCallback when inserting a custom element');
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ contentDocument.body.innerHTML = '<custom-element></custom-element>';
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+
+ contentDocument.open();
+ assert_array_equals(element.takeLog().types(), ['disconnected']);
+}, 'open on Document must enqueue disconnectedCallback when removing a custom element');
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ contentDocument.body.innerHTML = '<custom-element></custom-element>';
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+
+ contentDocument.write('');
+ assert_array_equals(element.takeLog().types(), ['disconnected']);
+}, 'write on Document must enqueue disconnectedCallback when removing a custom element');
+
+test_with_window(function (contentWindow, contentDocument) {
+ contentWindow.document.open();
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ contentWindow.document.write('<custom-element></custom-element>');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+}, 'write on Document must enqueue connectedCallback after constructing a custom element');
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ contentDocument.body.innerHTML = '<custom-element></custom-element>';
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+
+ contentDocument.writeln('');
+ assert_array_equals(element.takeLog().types(), ['disconnected']);
+}, 'writeln on Document must enqueue disconnectedCallback when removing a custom element');
+
+test_with_window(function (contentWindow) {
+ contentWindow.document.open();
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ contentWindow.document.writeln('<custom-element></custom-element>');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+}, 'writeln on Document must enqueue connectedCallback after constructing a custom element');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/reactions/Element.html b/testing/web-platform/tests/custom-elements/reactions/Element.html
new file mode 100644
index 0000000000..e1576734d0
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/Element.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: CEReactions on Element interface</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="id, className, slot, setAttribute, setAttributeNS, removeAttribute, removeAttributeNS, setAttributeNode, setAttributeNodeNS, removeAttributeNode, insertAdjacentElement, innerHTML, outerHTML, and insertAdjacentHTML of Element interface must have CEReactions">
+<meta name="help" content="https://dom.spec.whatwg.org/#element">
+<meta name="help" content="https://w3c.github.io/DOM-Parsing/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<script src="./resources/reactions.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+testReflectAttribute('id', 'id', 'foo', 'bar', 'id on Element');
+testReflectAttribute('className', 'class', 'foo', 'bar', 'className on Element');
+testReflectAttribute('slot', 'slot', 'foo', 'bar', 'slot on Element');
+
+testAttributeAdder(function (element, name, value) {
+ element.setAttribute(name, value);
+}, 'setAttribute on Element');
+
+testAttributeAdder(function (element, name, value) {
+ element.setAttributeNS(null, name, value);
+}, 'setAttributeNS on Element');
+
+testAttributeRemover(function (element, name) {
+ element.removeAttribute(name);
+}, 'removeAttribute on Element');
+
+testAttributeRemover(function (element, name) {
+ element.removeAttributeNS(null, name);
+}, 'removeAttributeNS on Element');
+
+testAttributeRemover(function (element, name, value) {
+ if (element.hasAttribute(name))
+ element.toggleAttribute(name);
+}, 'toggleAttribute (only removes) on Element');
+
+testAttributeRemover(function (element, name, value) {
+ element.toggleAttribute(name, false);
+}, 'toggleAttribute (force false) on Element');
+
+testAttributeAdder(function (element, name, value) {
+ var attr = document.createAttribute(name);
+ attr.value = value;
+ element.setAttributeNode(attr);
+}, 'setAttributeNode on Element');
+
+testAttributeAdder(function (element, name, value) {
+ var attr = document.createAttribute(name);
+ attr.value = value;
+ element.setAttributeNodeNS(attr);
+}, 'setAttributeNodeNS on Element');
+
+testAttributeRemover(function (element, name) {
+ var attr = element.getAttributeNode(name);
+ if (attr)
+ element.removeAttributeNode(element.getAttributeNode(name));
+}, 'removeAttributeNode on Element');
+
+testNodeConnector(function (newContainer, element) {
+ newContainer.insertAdjacentElement('afterBegin', element);
+}, 'insertAdjacentElement on Element');
+
+testInsertingMarkup(function (newContainer, markup) {
+ newContainer.innerHTML = markup;
+}, 'innerHTML on Element');
+
+testNodeDisconnector(function (customElement) {
+ customElement.parentNode.innerHTML = '';
+}, 'innerHTML on Element');
+
+testInsertingMarkup(function (newContainer, markup) {
+ newContainer.firstChild.outerHTML = markup;
+}, 'outerHTML on Element');
+
+testNodeDisconnector(function (customElement) {
+ customElement.outerHTML = '';
+}, 'outerHTML on Element');
+
+testInsertingMarkup(function (newContainer, markup) {
+ newContainer.insertAdjacentHTML('afterBegin', markup);
+}, 'insertAdjacentHTML on Element');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/reactions/ElementContentEditable.html b/testing/web-platform/tests/custom-elements/reactions/ElementContentEditable.html
new file mode 100644
index 0000000000..bdb10761cb
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/ElementContentEditable.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: CEReactions on ElementContentEditable interface</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="contentEditable of ElementContentEditable interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#elementcontenteditable">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<script src="./resources/reactions.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+testReflectAttribute('contentEditable', 'contenteditable', 'true', 'false', 'contentEditable on ElementContentEditable');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/reactions/HTMLAnchorElement.html b/testing/web-platform/tests/custom-elements/reactions/HTMLAnchorElement.html
new file mode 100644
index 0000000000..c6eeb1dce9
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/HTMLAnchorElement.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: CEReactions on HTMLAnchorElement interface</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="text of HTMLAnchorElement interface must have CEReactions">
+<meta name="help" content="https://dom.spec.whatwg.org/#node">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<script src="./resources/reactions.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ contentDocument.body.innerHTML = `<a><custom-element>hello</custom-element></a>`;
+ const anchor = contentDocument.querySelector('a');
+
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+ assert_equals(anchor.innerHTML, '<custom-element>hello</custom-element>');
+
+ anchor.text = 'world';
+ assert_equals(anchor.innerHTML, 'world');
+ assert_array_equals(element.takeLog().types(), ['disconnected']);
+}, 'text on HTMLAnchorElement must enqueue disconnectedCallback when removing a custom element');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/reactions/HTMLElement.html b/testing/web-platform/tests/custom-elements/reactions/HTMLElement.html
new file mode 100644
index 0000000000..0a1d40199e
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/HTMLElement.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: CEReactions on HTMLElement interface</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="title, lang, translate, dir, hidden, tabIndex, accessKey, draggable, dropzone, contextMenu, spellcheck, popover, innerText, and outerText of HTMLElement interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#htmlelement">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<script src="./resources/reactions.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+testReflectAttribute('title', 'title', 'foo', 'bar', 'title on HTMLElement');
+testReflectAttribute('lang', 'lang', 'en', 'zh', 'lang on HTMLElement');
+testReflectAttributeWithContentValues('translate', 'translate', true, 'yes', false, 'no', 'translate on HTMLElement');
+testReflectAttribute('dir', 'dir', 'ltr', 'rtl', 'dir on HTMLElement');
+testReflectBooleanAttribute('hidden', 'hidden', 'hidden on HTMLElement');
+testReflectAttribute('tabIndex', 'tabindex', '0', '1', 'tabIndex on HTMLElement');
+testReflectAttribute('accessKey', 'accesskey', 'a', 'b', 'accessKey on HTMLElement');
+testReflectAttributeWithContentValues('draggable', 'draggable', true, 'true', false, 'false', 'draggable on HTMLElement');
+testReflectAttributeWithContentValues('spellcheck', 'spellcheck', true, 'true', false, 'false', 'spellcheck on HTMLElement');
+testReflectAttribute('popover', 'popover', 'auto', 'manual', 'popover on HTMLElement');
+
+testNodeDisconnector(function (customElement) {
+ customElement.parentNode.innerText = '';
+}, 'innerText on HTMLElement');
+
+testNodeDisconnector(function (customElement) {
+ customElement.outerText = '';
+}, 'outerText on HTMLElement');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/reactions/HTMLOptionElement.html b/testing/web-platform/tests/custom-elements/reactions/HTMLOptionElement.html
new file mode 100644
index 0000000000..418ef282b3
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/HTMLOptionElement.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: CEReactions on HTMLOptionElement interface</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="text of HTMLOptionElement interface must have CEReactions">
+<meta name="help" content="https://dom.spec.whatwg.org/#node">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<script src="./resources/reactions.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ contentDocument.body.innerHTML = `<select><option></option></select>`;
+ const option = contentDocument.querySelector('option');
+ const instance = document.createElement('custom-element');
+ option.appendChild(instance);
+ instance.textContent = 'hello';
+
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+ assert_equals(option.innerHTML, '<custom-element>hello</custom-element>');
+
+ option.text = 'world';
+ assert_equals(option.innerHTML, 'world');
+ assert_array_equals(element.takeLog().types(), ['disconnected']);
+}, 'text on HTMLOptionElement must enqueue disconnectedCallback when removing a custom element');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/reactions/HTMLOptionsCollection.html b/testing/web-platform/tests/custom-elements/reactions/HTMLOptionsCollection.html
new file mode 100644
index 0000000000..0d64259d06
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/HTMLOptionsCollection.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: CEReactions on HTMLOptionsCollection interface</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="length, the indexed setter, add, and remove of HTMLOptionsCollection interface must have CEReactions">
+<meta name="help" content="https://dom.spec.whatwg.org/#node">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<script src="./resources/reactions.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ contentDocument.body.innerHTML = `<select><option></option></select>`;
+ const option = contentDocument.querySelector('option');
+
+ const instance = contentDocument.createElement(element.name);
+ option.appendChild(instance);
+ instance.textContent = 'hello';
+
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+ assert_equals(option.innerHTML, '<custom-element>hello</custom-element>');
+
+ const select = contentDocument.querySelector('select');
+ assert_equals(select.options[0], option);
+ select.options.length = 0;
+ assert_equals(select.firstChild, null);
+ assert_array_equals(element.takeLog().types(), ['disconnected']);
+}, 'length on HTMLOptionsCollection must enqueue disconnectedCallback when removing a custom element');
+
+test_with_window(function (contentWindow) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+
+ const contentDocument = contentWindow.document;
+ contentDocument.body.innerHTML = `<select></select>`;
+ const select = contentDocument.querySelector('select');
+
+ const option = contentDocument.createElement('option');
+ const instance = contentDocument.createElement(element.name);
+ option.appendChild(instance);
+ instance.textContent = 'hello';
+
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ assert_equals(option.innerHTML, '<custom-element>hello</custom-element>');
+
+ assert_equals(select.options.length, 0);
+ select.options[0] = option;
+ assert_equals(select.options.length, 1);
+ assert_array_equals(element.takeLog().types(), ['connected']);
+}, 'The indexed setter on HTMLOptionsCollection must enqueue connectedCallback when inserting a custom element');
+
+test_with_window(function (contentWindow) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+
+ const contentDocument = contentWindow.document;
+ contentDocument.body.innerHTML = `<select><option></option></select>`;
+ const option = contentDocument.querySelector('option');
+
+ const instance = contentDocument.createElement(element.name);
+ option.appendChild(instance);
+ instance.textContent = 'hello';
+
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+ assert_equals(option.innerHTML, '<custom-element>hello</custom-element>');
+
+ const select = contentDocument.querySelector('select');
+ assert_equals(select.options[0], option);
+ select.options[0] = null;
+ assert_equals(select.options.length, 0);
+ assert_array_equals(element.takeLog().types(), ['disconnected']);
+}, 'The indexed setter on HTMLOptionsCollection must enqueue disconnectedCallback when removing a custom element');
+
+test_with_window(function (contentWindow) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+
+ const contentDocument = contentWindow.document;
+ contentDocument.body.innerHTML = `<select></select>`;
+ const select = contentDocument.querySelector('select');
+
+ const option = contentDocument.createElement('option');
+ const instance = contentDocument.createElement(element.name);
+ option.appendChild(instance);
+ instance.textContent = 'hello';
+
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ assert_equals(option.innerHTML, '<custom-element>hello</custom-element>');
+
+ assert_equals(select.options.length, 0);
+ select.options.add(option);
+ assert_equals(select.options.length, 1);
+ assert_array_equals(element.takeLog().types(), ['connected']);
+}, 'add on HTMLOptionsCollection must enqueue connectedCallback when inserting a custom element');
+
+test_with_window(function (contentWindow) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+
+ const contentDocument = contentWindow.document;
+ contentDocument.body.innerHTML = `<select><option></option></select>`;
+ const option = contentDocument.querySelector('option');
+
+ const instance = contentDocument.createElement(element.name);
+ option.appendChild(instance);
+ instance.textContent = 'hello';
+
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+ assert_equals(option.innerHTML, '<custom-element>hello</custom-element>');
+
+ const select = contentDocument.querySelector('select');
+ assert_equals(select.options[0], option);
+ select.options.remove(0);
+ assert_equals(select.options.length, 0);
+ assert_array_equals(element.takeLog().types(), ['disconnected']);
+}, 'remove on HTMLOptionsCollection must enqueue disconnectedCallback when removing a custom element');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/reactions/HTMLOutputElement.html b/testing/web-platform/tests/custom-elements/reactions/HTMLOutputElement.html
new file mode 100644
index 0000000000..02e669bc7a
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/HTMLOutputElement.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: CEReactions on HTMLOutputElement interface</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="value and defaultValue of HTMLOutputElement interface must have CEReactions">
+<meta name="help" content="https://dom.spec.whatwg.org/#node">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<script src="./resources/reactions.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ contentDocument.body.innerHTML = `<output><custom-element>hello</custom-element></output>`;
+ const anchor = contentDocument.querySelector('output');
+
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+ assert_equals(anchor.innerHTML, '<custom-element>hello</custom-element>');
+
+ anchor.value = 'world';
+ assert_equals(anchor.innerHTML, 'world');
+ assert_array_equals(element.takeLog().types(), ['disconnected']);
+}, 'value on HTMLOutputElement must enqueue disconnectedCallback when removing a custom element');
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ contentDocument.body.innerHTML = `<output><custom-element>hello</custom-element></output>`;
+ const anchor = contentDocument.querySelector('output');
+
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+ assert_equals(anchor.innerHTML, '<custom-element>hello</custom-element>');
+
+ anchor.defaultValue = 'world';
+ assert_equals(anchor.innerHTML, 'world');
+ assert_array_equals(element.takeLog().types(), ['disconnected']);
+}, 'defaultValue on HTMLOutputElement must enqueue disconnectedCallback when removing a custom element');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/reactions/HTMLSelectElement.html b/testing/web-platform/tests/custom-elements/reactions/HTMLSelectElement.html
new file mode 100644
index 0000000000..7c79634f66
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/HTMLSelectElement.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: CEReactions on HTMLSelectElement interface</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="length, add, remove, and the setter of HTMLSelectElement interface must have CEReactions">
+<meta name="help" content="https://dom.spec.whatwg.org/#node">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<script src="./resources/reactions.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ contentDocument.body.innerHTML = `<select><option></option></select>`;
+ const option = contentDocument.querySelector('option');
+
+ const instance = contentDocument.createElement(element.name);
+ option.appendChild(instance);
+ instance.textContent = 'hello';
+
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+ assert_equals(option.innerHTML, '<custom-element>hello</custom-element>');
+
+ const select = contentDocument.querySelector('select');
+ assert_equals(select.length, 1);
+ select.length = 0;
+ assert_equals(select.firstChild, null);
+ assert_array_equals(element.takeLog().types(), ['disconnected']);
+}, 'length on HTMLSelectElement must enqueue disconnectedCallback when removing a custom element');
+
+test_with_window(function (contentWindow) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+
+ const contentDocument = contentWindow.document;
+ contentDocument.body.innerHTML = `<select></select>`;
+ const select = contentDocument.querySelector('select');
+
+ const option = contentDocument.createElement('option');
+ const instance = contentDocument.createElement(element.name);
+ option.appendChild(instance);
+ instance.textContent = 'hello';
+
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ assert_equals(option.innerHTML, '<custom-element>hello</custom-element>');
+
+ assert_equals(select.options.length, 0);
+ select[0] = option;
+ assert_equals(select.options.length, 1);
+ assert_array_equals(element.takeLog().types(), ['connected']);
+}, 'The indexed setter on HTMLSelectElement must enqueue connectedCallback when inserting a custom element');
+
+test_with_window(function (contentWindow) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+
+ const contentDocument = contentWindow.document;
+ contentDocument.body.innerHTML = `<select><option></option></select>`;
+ const option = contentDocument.querySelector('option');
+
+ const instance = contentDocument.createElement(element.name);
+ option.appendChild(instance);
+ instance.textContent = 'hello';
+
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+ assert_equals(option.innerHTML, '<custom-element>hello</custom-element>');
+
+ const select = contentDocument.querySelector('select');
+ assert_equals(select.options[0], option);
+ select[0] = null;
+ assert_equals(select.options.length, 0);
+ assert_array_equals(element.takeLog().types(), ['disconnected']);
+}, 'The indexed setter on HTMLSelectElement must enqueue disconnectedCallback when removing a custom element');
+
+test_with_window(function (contentWindow) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+
+ const contentDocument = contentWindow.document;
+ contentDocument.body.innerHTML = `<select></select>`;
+ const select = contentDocument.querySelector('select');
+
+ const option = contentDocument.createElement('option');
+ const instance = contentDocument.createElement(element.name);
+ option.appendChild(instance);
+ instance.textContent = 'hello';
+
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ assert_equals(option.innerHTML, '<custom-element>hello</custom-element>');
+
+ assert_equals(select.options.length, 0);
+ select.add(option);
+ assert_equals(select.options.length, 1);
+ assert_array_equals(element.takeLog().types(), ['connected']);
+}, 'add on HTMLSelectElement must enqueue connectedCallback when inserting a custom element');
+
+test_with_window(function (contentWindow) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+
+ const contentDocument = contentWindow.document;
+ contentDocument.body.innerHTML = `<select><option></option></select>`;
+ const option = contentDocument.querySelector('option');
+
+ const instance = contentDocument.createElement(element.name);
+ option.appendChild(instance);
+ instance.textContent = 'hello';
+
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+ assert_equals(option.innerHTML, '<custom-element>hello</custom-element>');
+
+ const select = contentDocument.querySelector('select');
+ assert_equals(select.options[0], option);
+ select.remove(0);
+ assert_equals(select.options.length, 0);
+ assert_array_equals(element.takeLog().types(), ['disconnected']);
+}, 'remove on HTMLSelectElement must enqueue disconnectedCallback when removing a custom element');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/reactions/HTMLTableElement.html b/testing/web-platform/tests/custom-elements/reactions/HTMLTableElement.html
new file mode 100644
index 0000000000..6adf2623d6
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/HTMLTableElement.html
@@ -0,0 +1,173 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: CEReactions on HTMLTableElement interface</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="caption, deleteCaption, thead, deleteTHead, tFoot, deleteTFoot, and deleteRow of HTMLTableElement interface must have CEReactions">
+<meta name="help" content="https://dom.spec.whatwg.org/#node">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<script src="./resources/reactions.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ contentDocument.body.innerHTML = `<table></table>`;
+ const table = contentDocument.querySelector('table');
+
+ const caption = contentDocument.createElement('caption');
+ caption.innerHTML = '<custom-element>hello</custom-element>';
+
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ assert_equals(caption.innerHTML, '<custom-element>hello</custom-element>');
+
+ assert_equals(table.caption, null);
+ table.caption = caption;
+ assert_array_equals(element.takeLog().types(), ['connected']);
+}, 'caption on HTMLTableElement must enqueue connectedCallback when inserting a custom element');
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ contentDocument.body.innerHTML = `<table><caption><custom-element>hello</custom-element></caption></table>`;
+ const caption = contentDocument.querySelector('caption');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+ assert_equals(caption.innerHTML, '<custom-element>hello</custom-element>');
+
+ const table = contentDocument.querySelector('table');
+ assert_equals(table.caption, caption);
+ const newCaption = contentDocument.createElement('caption');
+ table.caption = newCaption; // Chrome doesn't support setting to null.
+ assert_equals(table.caption, newCaption);
+ assert_array_equals(element.takeLog().types(), ['disconnected']);
+}, 'caption on HTMLTableElement must enqueue disconnectedCallback when removing a custom element');
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ contentDocument.body.innerHTML = `<table><caption><custom-element>hello</custom-element></caption></table>`;
+ const caption = contentDocument.querySelector('caption');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+ assert_equals(caption.innerHTML, '<custom-element>hello</custom-element>');
+
+ const table = contentDocument.querySelector('table');
+ assert_equals(table.caption, caption);
+ const newCaption = contentDocument.createElement('caption');
+ table.deleteCaption();
+ assert_equals(table.caption, null);
+ assert_array_equals(element.takeLog().types(), ['disconnected']);
+}, 'deleteCaption() on HTMLTableElement must enqueue disconnectedCallback when removing a custom element');
+
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ contentDocument.body.innerHTML = `<table></table>`;
+ const table = contentDocument.querySelector('table');
+
+ const thead = contentDocument.createElement('thead');
+ thead.innerHTML = '<tr><td><custom-element>hello</custom-element></td></tr>';
+
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ assert_equals(thead.innerHTML, '<tr><td><custom-element>hello</custom-element></td></tr>');
+
+ assert_equals(table.tHead, null);
+ table.tHead = thead;
+ assert_array_equals(element.takeLog().types(), ['connected']);
+}, 'tHead on HTMLTableElement must enqueue connectedCallback when inserting a custom element');
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ contentDocument.body.innerHTML = `<table><thead><tr><td><custom-element>hello</custom-element></td></tr></thead></table>`;
+ const thead = contentDocument.querySelector('thead');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+ assert_equals(thead.innerHTML, '<tr><td><custom-element>hello</custom-element></td></tr>');
+
+ const table = contentDocument.querySelector('table');
+ assert_equals(table.tHead, thead);
+ const newThead = contentDocument.createElement('thead');
+ table.tHead = newThead; // Chrome doesn't support setting to null.
+ assert_equals(table.tHead, newThead);
+ assert_array_equals(element.takeLog().types(), ['disconnected']);
+}, 'tHead on HTMLTableElement must enqueue disconnectedCallback when removing a custom element');
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ contentDocument.body.innerHTML = `<table><thead><tr><td><custom-element>hello</custom-element></td></tr></thead></table>`;
+ const thead = contentDocument.querySelector('thead');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+ assert_equals(thead.innerHTML, '<tr><td><custom-element>hello</custom-element></td></tr>');
+
+ const table = contentDocument.querySelector('table');
+ assert_equals(table.tHead, thead);
+ table.deleteTHead();
+ assert_equals(table.tHead, null);
+ assert_array_equals(element.takeLog().types(), ['disconnected']);
+}, 'deleteTHead() on HTMLTableElement must enqueue disconnectedCallback when removing a custom element');
+
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ contentDocument.body.innerHTML = `<table></table>`;
+ const table = contentDocument.querySelector('table');
+
+ const tfoot = contentDocument.createElement('tfoot');
+ tfoot.innerHTML = '<tr><td><custom-element>hello</custom-element></td></tr>';
+
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ assert_equals(tfoot.innerHTML, '<tr><td><custom-element>hello</custom-element></td></tr>');
+
+ assert_equals(table.tFoot, null);
+ table.tFoot = tfoot;
+ assert_array_equals(element.takeLog().types(), ['connected']);
+}, 'tFoot on HTMLTableElement must enqueue connectedCallback when inserting a custom element');
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ contentDocument.body.innerHTML = `<table><tfoot><tr><td><custom-element>hello</custom-element></td></tr></tfoot></table>`;
+ const tfoot = contentDocument.querySelector('tfoot');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+ assert_equals(tfoot.innerHTML, '<tr><td><custom-element>hello</custom-element></td></tr>');
+
+ const table = contentDocument.querySelector('table');
+ assert_equals(table.tFoot, tfoot);
+ const newThead = contentDocument.createElement('tfoot');
+ table.tFoot = newThead; // Chrome doesn't support setting to null.
+ assert_equals(table.tFoot, newThead);
+ assert_array_equals(element.takeLog().types(), ['disconnected']);
+}, 'tFoot on HTMLTableElement must enqueue disconnectedCallback when removing a custom element');
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ contentDocument.body.innerHTML = `<table><tfoot><tr><td><custom-element>hello</custom-element></td></tr></tfoot></table>`;
+ const tfoot = contentDocument.querySelector('tfoot');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+ assert_equals(tfoot.innerHTML, '<tr><td><custom-element>hello</custom-element></td></tr>');
+
+ const table = contentDocument.querySelector('table');
+ assert_equals(table.tFoot, tfoot);
+ table.deleteTFoot();
+ assert_equals(table.tFoot, null);
+ assert_array_equals(element.takeLog().types(), ['disconnected']);
+}, 'deleteTFoot() on HTMLTableElement must enqueue disconnectedCallback when removing a custom element');
+
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ contentDocument.body.innerHTML = `<table><tr><td><custom-element>hello</custom-element></td></tr></table>`;
+ const tr = contentDocument.querySelector('tr');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+ assert_equals(tr.innerHTML, '<td><custom-element>hello</custom-element></td>');
+
+ const table = contentDocument.querySelector('table');
+ assert_equals(table.rows.length, 1);
+ assert_equals(table.rows[0], tr);
+ table.deleteRow(0);
+ assert_equals(table.rows.length, 0);
+ assert_array_equals(element.takeLog().types(), ['disconnected']);
+}, 'deleteRow() on HTMLTableElement must enqueue disconnectedCallback when removing a custom element');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/reactions/HTMLTableRowElement.html b/testing/web-platform/tests/custom-elements/reactions/HTMLTableRowElement.html
new file mode 100644
index 0000000000..a9a00a5da3
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/HTMLTableRowElement.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: CEReactions on HTMLTableRowElement interface</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="deleteCell of HTMLTableRowElement interface must have CEReactions">
+<meta name="help" content="https://dom.spec.whatwg.org/#node">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<script src="./resources/reactions.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ contentDocument.body.innerHTML = `<table><tr><td><custom-element>hello</custom-element></td></tr></table>`;
+ const td = contentDocument.querySelector('td');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+ assert_equals(td.innerHTML, '<custom-element>hello</custom-element>');
+
+ const table = contentDocument.querySelector('table');
+ const row = table.rows[0];
+ assert_equals(row.cells[0], td);
+ row.deleteCell(0);
+ assert_equals(row.cells.length, 0);
+ assert_array_equals(element.takeLog().types(), ['disconnected']);
+}, 'deleteCell() on HTMLTableRowElement must enqueue disconnectedCallback when removing a custom element');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/reactions/HTMLTableSectionElement.html b/testing/web-platform/tests/custom-elements/reactions/HTMLTableSectionElement.html
new file mode 100644
index 0000000000..cbb0a146e8
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/HTMLTableSectionElement.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: CEReactions on HTMLTableSectionElement interface</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="deleteRow of HTMLTableSectionElement interface must have CEReactions">
+<meta name="help" content="https://dom.spec.whatwg.org/#node">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<script src="./resources/reactions.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ contentDocument.body.innerHTML = `<table><thead><tr><td><custom-element>hello</custom-element></td></tr></thead></table>`;
+ const thead = contentDocument.querySelector('thead');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+ assert_equals(thead.innerHTML, '<tr><td><custom-element>hello</custom-element></td></tr>');
+
+ const table = contentDocument.querySelector('table');
+ assert_equals(table.tHead, thead);
+ table.tHead.deleteRow(0);
+ assert_array_equals(element.takeLog().types(), ['disconnected']);
+}, 'deleteRow() on HTMLTableSectionElement on thead must enqueue disconnectedCallback when removing a custom element');
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ contentDocument.body.innerHTML = `<table><tfoot><tr><td><custom-element>hello</custom-element></td></tr></tfoot></table>`;
+ const tfoot = contentDocument.querySelector('tfoot');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+ assert_equals(tfoot.innerHTML, '<tr><td><custom-element>hello</custom-element></td></tr>');
+
+ const table = contentDocument.querySelector('table');
+ assert_equals(table.tFoot, tfoot);
+ table.tFoot.deleteRow(0);
+ assert_array_equals(element.takeLog().types(), ['disconnected']);
+}, 'deleteRow() on HTMLTableSectionElement on tfoot must enqueue disconnectedCallback when removing a custom element');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/reactions/HTMLTitleElement.html b/testing/web-platform/tests/custom-elements/reactions/HTMLTitleElement.html
new file mode 100644
index 0000000000..6678944c91
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/HTMLTitleElement.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: CEReactions on HTMLTitleElement interface</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="text of HTMLTitleElement interface must have CEReactions">
+<meta name="help" content="https://dom.spec.whatwg.org/#node">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<script src="./resources/reactions.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ const instance = contentWindow.document.createElement(element.name);
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+
+ contentWindow.document.title = 'hello';
+ const titleElement = contentDocument.querySelector('title');
+ titleElement.appendChild(instance);
+ assert_array_equals(element.takeLog().types(), ['connected']);
+ assert_equals(titleElement.childNodes.length, 2);
+
+ titleElement.text = 'world';
+ assert_equals(titleElement.childNodes.length, 1);
+ assert_array_equals(element.takeLog().types(), ['disconnected']);
+}, 'text on HTMLTitleElement must enqueue disconnectedCallback when removing a custom element');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/reactions/NamedNodeMap.html b/testing/web-platform/tests/custom-elements/reactions/NamedNodeMap.html
new file mode 100644
index 0000000000..fa21b3ada9
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/NamedNodeMap.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: CEReactions on NamedNodeMap interface</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="setNamedItem, setNamedItemNS, removeNameditem, and removeNamedItemNS of NamedNodeMap interface must have CEReactions">
+<meta name="help" content="https://dom.spec.whatwg.org/#node">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<script src="./resources/reactions.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+testAttributeAdder(function (element, name, value) {
+ var attr = element.ownerDocument.createAttribute(name);
+ attr.value = value;
+ element.attributes.setNamedItem(attr);
+}, 'setNamedItem on NamedNodeMap');
+
+testAttributeAdder(function (element, name, value) {
+ var attr = element.ownerDocument.createAttribute(name);
+ attr.value = value;
+ element.attributes.setNamedItemNS(attr);
+}, 'setNamedItemNS on NamedNodeMap');
+
+testAttributeRemover(function (element, name) {
+ element.attributes.removeNamedItem(name);
+}, 'removeNamedItem on NamedNodeMap', {onlyExistingAttribute: true});
+
+testAttributeRemover(function (element, name) {
+ element.attributes.removeNamedItemNS(null, name);
+}, 'removeNamedItemNS on NamedNodeMap', {onlyExistingAttribute: true});
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/reactions/Node.html b/testing/web-platform/tests/custom-elements/reactions/Node.html
new file mode 100644
index 0000000000..94da3d020e
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/Node.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: CEReactions on Node interface</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="nodeValue, textContent, normalize, cloneNode, insertBefore, appendChild, replaceChild, and removeChild of Node interface must have CEReactions">
+<meta name="help" content="https://dom.spec.whatwg.org/#node">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<script src="./resources/reactions.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+testAttributeMutator(function (element, name, value) {
+ element.getAttributeNode(name).nodeValue = value;
+}, 'nodeValue on Node');
+
+testAttributeMutator(function (element, name, value) {
+ element.getAttributeNode(name).textContent = value;
+}, 'textContent on Node');
+
+// FIXME: Add a test for normalize()
+
+testCloner(function (customElement) {
+ return customElement.cloneNode(false);
+}, 'cloneNode on Node');
+
+testNodeConnector(function (newContainer, customElement) {
+ newContainer.insertBefore(customElement, newContainer.firstChild);
+}, 'insertBefore on ChildNode');
+
+testNodeConnector(function (newContainer, customElement) {
+ newContainer.appendChild(customElement);
+}, 'appendChild on ChildNode');
+
+testNodeConnector(function (newContainer, customElement) {
+ newContainer.replaceChild(customElement, newContainer.firstChild);
+}, 'replaceChild on ChildNode');
+
+testNodeDisconnector(function (customElement) {
+ customElement.parentNode.removeChild(customElement);
+}, 'removeChild on ChildNode');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/reactions/ParentNode.html b/testing/web-platform/tests/custom-elements/reactions/ParentNode.html
new file mode 100644
index 0000000000..b143b5a982
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/ParentNode.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: CEReactions on ParentNode interface</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="prepend and append of ParentNode interface must have CEReactions">
+<meta name="help" content="https://dom.spec.whatwg.org/#parentnode">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<script src="./resources/reactions.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+testNodeConnector(function (newContainer, customElement) {
+ newContainer.prepend(customElement);
+}, 'prepend on ParentNode');
+
+testNodeConnector(function (newContainer, customElement) {
+ newContainer.append(customElement);
+}, 'append on ParentNode');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/reactions/Range.html b/testing/web-platform/tests/custom-elements/reactions/Range.html
new file mode 100644
index 0000000000..c4a8252ff6
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/Range.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: CEReactions on Range interface</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="deleteContents, extractContents, cloneContents, insertNode, and surroundContents of Range interface must have CEReactions">
+<meta name="help" content="https://dom.spec.whatwg.org/#node">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<script src="./resources/reactions.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+testNodeDisconnector(function (customElement) {
+ var range = document.createRange();
+ range.selectNode(customElement);
+ range.deleteContents();
+}, 'deleteContents on Range');
+
+testNodeDisconnector(function (customElement) {
+ var range = document.createRange();
+ range.selectNode(customElement);
+ range.extractContents();
+}, 'extractContents on Range');
+
+testCloner(function (customElement) {
+ var range = document.createRange();
+ range.selectNode(customElement);
+ range.cloneContents();
+}, 'cloneContents on Range')
+
+testNodeConnector(function (container, customElement) {
+ var range = document.createRange();
+ range.selectNodeContents(container);
+ range.insertNode(customElement);
+}, 'insertNode on Range');
+
+testNodeConnector(function (container, customElement) {
+ var range = document.createRange();
+ range.selectNodeContents(container);
+ range.surroundContents(customElement);
+}, 'surroundContents on Range');
+
+testParsingMarkup(function (document, markup) {
+ var range = document.createRange();
+ return range.createContextualFragment(markup);
+}, 'createContextualFragment on Range');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/reactions/Selection.html b/testing/web-platform/tests/custom-elements/reactions/Selection.html
new file mode 100644
index 0000000000..84214201aa
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/Selection.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: CEReactions on Selection interface</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="deleteFromDocument of Selection interface must have CEReactions">
+<meta name="help" content="http://w3c.github.io/selection-api/#selection-interface">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<script src="./resources/reactions.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+testNodeDisconnector(function (customElement, window) {
+ let selection = window.getSelection();
+ let parent = customElement.parentNode;
+
+ // WebKit and Blink "normalizes" selection in selectAllChildren and not select the empty customElement.
+ // Workaround this orthogonal non-standard behavior by inserting text nodes around the custom element.
+ parent.prepend(document.createTextNode('start'));
+ parent.append(document.createTextNode('end'));
+
+ selection.selectAllChildren(parent);
+ selection.deleteFromDocument();
+}, 'deleteFromDocument on Selection');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/reactions/ShadowRoot.html b/testing/web-platform/tests/custom-elements/reactions/ShadowRoot.html
new file mode 100644
index 0000000000..9997d9c836
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/ShadowRoot.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: CEReactions on ShadowRoot interface</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="innerHTML of ShadowRoot interface must have CEReactions">
+<meta name="help" content="https://dom.spec.whatwg.org/#node">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<script src="./resources/reactions.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ const host = contentDocument.createElement('div');
+ const shadowRoot = host.attachShadow({mode: 'closed'});
+ shadowRoot.innerHTML = '<custom-element></custom-element>';
+
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+}, 'innerHTML on ShadowRoot must upgrade a custom element');
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ const host = contentDocument.createElement('div');
+ contentDocument.body.appendChild(host);
+ const shadowRoot = host.attachShadow({mode: 'closed'});
+ shadowRoot.innerHTML = '<custom-element></custom-element>';
+
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+}, 'innerHTML on ShadowRoot must enqueue connectedCallback on newly upgraded custom elements when the shadow root is connected');
+
+test_with_window(function (contentWindow, contentDocument) {
+ const element = define_custom_element_in_window(contentWindow, 'custom-element', []);
+ const host = contentDocument.createElement('div');
+ contentDocument.body.appendChild(host);
+
+ const shadowRoot = host.attachShadow({mode: 'closed'});
+ shadowRoot.innerHTML = '<custom-element></custom-element>';
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+
+ shadowRoot.innerHTML = '';
+ assert_array_equals(element.takeLog().types(), ['disconnected']);
+
+}, 'innerHTML on ShadowRoot must enqueue disconnectedCallback when removing a custom element');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLAreaElement.html b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLAreaElement.html
new file mode 100644
index 0000000000..3d53ff87ff
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLAreaElement.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<title>Custom Elements: CEReactions on HTMLAreaElement interface</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<meta name="assert" content="alt, coords, shape, target, download, ping, rel,
+ referrerPolicy of HTMLAreaElement interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#the-area-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/custom-elements-helpers.js"></script>
+<script src="../resources/reactions.js"></script>
+
+<map name="yellow" id="map">
+</map>
+<img usemap="#yellow" src="/images/yellow.png" alt="yellow pic">
+
+<script>
+
+function getParentElement() {
+ let map = document.getElementById('map');
+ return map;
+}
+
+function setAttributes(instance) {
+ instance.setAttribute('href', '/images/yellow.png');
+}
+
+testReflectAttributeWithDependentAttributes(
+ 'alt', 'alt', 'yellow pic',
+ 'yellow pic2', 'alt on HTMLAreaElement', 'area',
+ getParentElement, instance => setAttributes(instance), HTMLAreaElement
+);
+testReflectAttributeWithParentNode(
+ 'coords', 'coords', '1, 1, 5, 5',
+ '2, 2, 6, 6', 'coords on HTMLAreaElement', 'area',
+ getParentElement, HTMLAreaElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'shape', 'shape', 'rectangle',
+ 'default', 'shape on HTMLAreaElement', 'area',
+ getParentElement, instance => instance.setAttribute('coords', '1, 1, 5, 5'),
+ HTMLAreaElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'target', 'target', '_blank',
+ '_top', 'target on HTMLAreaElement', 'area',
+ getParentElement, instance => setAttributes(instance), HTMLAreaElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'download', 'download', 'pic1',
+ 'pic2', 'download on HTMLAreaElement', 'area',
+ getParentElement, instance => setAttributes(instance), HTMLAreaElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'ping', 'ping', 'location.href',
+ `${location.protocol}\/\/${location.host}`, 'ping on HTMLAreaElement', 'area',
+ getParentElement, instance => setAttributes(instance), HTMLAreaElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'rel', 'rel', 'help',
+ 'noreferrer', 'rel on HTMLAreaElement', 'area',
+ getParentElement, instance => setAttributes(instance), HTMLAreaElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'referrerPolicy', 'referrerpolicy', 'same-origin',
+ 'origin', 'referrerPolicy on HTMLAreaElement', 'area',
+ getParentElement, instance => setAttributes(instance), HTMLAreaElement
+);
+
+</script>
diff --git a/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLBaseElement.html b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLBaseElement.html
new file mode 100644
index 0000000000..8d8470074c
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLBaseElement.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<head>
+<title>Custom Elements: CEReactions on HTMLBaseElement interface</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<meta name="assert" content="href, target of HTMLBaseElement interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#the-base-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/custom-elements-helpers.js"></script>
+<script src="../resources/reactions.js"></script>
+</head>
+<script>
+
+function getParentElement() {
+ return document.head;
+}
+
+testReflectAttributeWithParentNode('href', 'href', '/', 'http://example.com/', 'href on HTMLBaseElement', 'base', getParentElement, HTMLBaseElement);
+testReflectAttributeWithParentNode('target', 'target', '_blank', '_self', 'target on HTMLBaseElement', 'base', getParentElement, HTMLBaseElement);
+
+</script>
diff --git a/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLButtonElement.html b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLButtonElement.html
new file mode 100644
index 0000000000..62f8b7b0c9
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLButtonElement.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<title>Custom Elements: CEReactions on HTMLButtonElement interface</title>
+<meta name="author" title="Zhang Xiaoyu" href="xiaoyux.zhang@intel.com">
+<meta name="assert" content=" autofocus, disabled, formAction, formEnctype,
+ formMethod, formNoValidate, formTarget, name, type, value
+ of HTMLButtonElement interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#htmlbuttonelement">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/custom-elements-helpers.js"></script>
+<script src="../resources/reactions.js"></script>
+<body>
+<script>
+
+function getParentElement(parentElementName) {
+ let parentElement = document.createElement(parentElementName);
+ document.body.appendChild(parentElement);
+ return parentElement;
+}
+
+function setAttributes(instance) {
+ instance.setAttribute('type', 'submit');
+}
+
+testReflectBooleanAttribute(
+ 'autofocus', 'autofocus', 'autofocus on HTMLButtonElement',
+ 'button', HTMLButtonElement
+);
+testReflectBooleanAttribute(
+ 'disabled', 'disabled','disabled on HTMLButtonElement',
+ 'button', HTMLButtonElement
+);
+testReflectAttribute(
+ 'name', 'name', 'intel',
+ 'intel1', 'name on HTMLButtonElement', 'button',
+ HTMLButtonElement
+);
+testReflectAttribute(
+ 'value', 'value', 'HTML',
+ 'CSS', 'value on HTMLButtonElement', 'button',
+ HTMLButtonElement
+);
+testReflectAttributeWithParentNode(
+ 'type', 'type', 'submit',
+ 'reset', 'type on HTMLButtonElement', 'button',
+ () => getParentElement('form'), HTMLButtonElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'formAction', 'formaction', 'intel.asp',
+ 'intel1.asp', 'formAction on HTMLButtonElement', 'button',
+ () => getParentElement('form'), instance => setAttributes(instance),
+ HTMLButtonElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'formEnctype', 'formenctype', 'text/plain', 'multipart/form-data',
+ 'formEnctype on HTMLButtonElement', 'button', () => getParentElement('form'),
+ instance => setAttributes(instance),
+ HTMLButtonElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'formMethod', 'formmethod', 'get',
+ 'post', 'formMethod on HTMLButtonElement', 'button',
+ () => getParentElement('form'), instance => setAttributes(instance),
+ HTMLButtonElement
+);
+testReflectBooleanAttributeWithDependentAttributes(
+ 'formNoValidate', 'formnovalidate', 'formNoValidate on HTMLButtonElement',
+ 'button', () => getParentElement('form'),
+ instance => setAttributes(instance),
+ HTMLButtonElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'formTarget', 'formtarget', '_blank',
+ '_self', 'formTarget on HTMLButtonElement', 'button',
+ () => getParentElement('form'), instance => setAttributes(instance),
+ HTMLButtonElement
+);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLCanvasElement.html b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLCanvasElement.html
new file mode 100644
index 0000000000..6c7119252e
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLCanvasElement.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<title>Custom Elements: CEReactions on HTMLCanvasElement interface</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<meta name="assert" content="width, height of HTMLCanvasElement interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#the-canvas-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/custom-elements-helpers.js"></script>
+<script src="../resources/reactions.js"></script>
+
+<script>
+
+testReflectAttribute('width', 'width', '15', '20', 'width on HTMLCanvasElement', 'canvas', HTMLCanvasElement);
+testReflectAttribute('height', 'height', '23', '45', 'height on HTMLCanvasElement', 'canvas', HTMLCanvasElement);
+
+</script>
diff --git a/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLDataElement.html b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLDataElement.html
new file mode 100644
index 0000000000..f078c6aa02
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLDataElement.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<title>Custom Elements: CEReactions on HTMLDataElement interface</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<meta name="assert" content="value of HTMLDataElement interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#the-data-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/custom-elements-helpers.js"></script>
+<script src="../resources/reactions.js"></script>
+
+<script>
+
+testReflectAttribute('value', 'value', '1234', '2345', 'name on HTMLDataElement', 'data', HTMLDataElement);
+
+</script>
diff --git a/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLDetailsElement.html b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLDetailsElement.html
new file mode 100644
index 0000000000..4d81e3e627
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLDetailsElement.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<title>Custom Elements: CEReactions on HTMLDetailsElement interface</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<meta name="assert" content="open of HTMLDetailsElement interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#the-details-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/custom-elements-helpers.js"></script>
+<script src="../resources/reactions.js"></script>
+
+<script>
+
+testReflectBooleanAttribute('open', 'open', 'open on HTMLDetailsElement', 'details', HTMLDetailsElement);
+
+</script>
diff --git a/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLEmbedElement.html b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLEmbedElement.html
new file mode 100644
index 0000000000..923bde7583
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLEmbedElement.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<title>Custom Elements: CEReactions on HTMLEmbedElement interface</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<meta name="assert" content="src, type, width, height of
+ HTMLEmbedElement interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#the-embed-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/custom-elements-helpers.js"></script>
+<script src="../resources/reactions.js"></script>
+
+<script>
+
+testReflectAttribute(
+ 'src', 'src', '/media/movie_5.mp4',
+ '/media/sound_5.mp3', 'src on HTMLEmbedElement', 'embed',
+ HTMLEmbedElement
+);
+testReflectAttribute(
+ 'type', 'type', 'video/webm',
+ 'video/mp4', 'type on HTMLEmbedElement', 'embed',
+ HTMLEmbedElement
+);
+testReflectAttribute(
+ 'width', 'width', '100',
+ '120', 'width on HTMLEmbedElement', 'embed',
+ HTMLEmbedElement
+);
+testReflectAttribute(
+ 'height', 'height', '100',
+ '120', 'height on HTMLEmbedElement', 'embed',
+ HTMLEmbedElement
+);
+
+</script>
diff --git a/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLFieldSetElement.html b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLFieldSetElement.html
new file mode 100644
index 0000000000..517523551b
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLFieldSetElement.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<title>Custom Elements: CEReactions on HTMLFieldSetElement interface</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<meta name="assert" content="disabled, name of
+ HTMLFieldSetElement interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#the-fieldset-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/custom-elements-helpers.js"></script>
+<script src="../resources/reactions.js"></script>
+
+<body>
+<script>
+
+function getParentElement() {
+ let form = document.createElement("form");
+ document.body.appendChild(form);
+ return form;
+}
+
+testReflectBooleanAttributeWithParentNode(
+ 'disabled', 'disabled', 'disabled on HTMLFieldSetElement',
+ 'fieldset', getParentElement, HTMLFieldSetElement
+);
+testReflectAttributeWithParentNode(
+ 'name', 'name', 'fieldset1',
+ 'fieldset2', 'name on HTMLFieldSetElement', 'fieldset',
+ getParentElement, HTMLFieldSetElement
+);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLImageElement.html b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLImageElement.html
new file mode 100644
index 0000000000..656e29eb17
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLImageElement.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<title>Custom Elements: CEReactions on HTMLImageElement interface</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<meta name="assert" content="alt, src, srcset, sizes, crossOrigin, useMap,
+ isMap, width, height, referrerPolicy, decoding of
+ HTMLImageElement interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#the-img-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/custom-elements-helpers.js"></script>
+<script src="../resources/reactions.js"></script>
+
+<map name="yellow"></map>
+<map name="green"></map>
+<a href="/" id="a">
+</a>
+<body>
+<script>
+
+function getParentElement() {
+ return document.body;
+}
+
+function setAttributes(instance) {
+ instance.setAttribute('src', '/images/green-1x1.png');
+}
+
+testReflectAttributeWithDependentAttributes(
+ 'alt', 'alt', 'image1',
+ 'image2', 'alt on HTMLImageElement', 'img',
+ getParentElement, instance => setAttributes(instance), HTMLImageElement
+);
+testReflectAttributeWithParentNode(
+ 'src', 'src', '/images/green-1x1.png',
+ '/images/green-2x2.png', 'src on HTMLImageElement', 'img',
+ getParentElement, HTMLImageElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'srcset', 'srcset', '/images/green.png',
+ '/images/green-2x2.png', 'srcset on HTMLImageElement', 'img',
+ getParentElement, instance => setAttributes(instance), HTMLImageElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'sizes', 'sizes', '(max-width: 32px) 28px',
+ '(max-width: 48px) 44px', 'sizes on HTMLImageElement', 'img',
+ getParentElement, instance => {
+ instance.setAttribute('src', '/images/green-1x1.png');
+ instance.setAttribute('srcset', '/images/green-2x2.png 1x');
+ }, HTMLImageElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'crossOrigin', 'crossorigin', 'use-credentials',
+ 'anonymous', 'crossOrigin on HTMLImageElement', 'img',
+ getParentElement, instance => setAttributes(instance), HTMLImageElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'useMap', 'usemap', '#yellow',
+ '#green', 'useMap on HTMLImageElement', 'img',
+ getParentElement, instance => setAttributes(instance), HTMLImageElement
+);
+testReflectBooleanAttributeWithDependentAttributes(
+ 'isMap', 'ismap', 'isMap on HTMLImageElement',
+ 'img', () => { return document.getElementById('a') },
+ instance => setAttributes(instance),
+ HTMLImageElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'width', 'width', '1',
+ '2', 'width on HTMLImageElement', 'img',
+ getParentElement, instance => setAttributes(instance), HTMLImageElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'height', 'height', '1',
+ '2', 'height on HTMLImageElement', 'img',
+ getParentElement, instance => setAttributes(instance), HTMLImageElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'referrerPolicy', 'referrerpolicy', 'same-origin',
+ 'origin', 'referrerPolicy on HTMLImageElement', 'img',
+ getParentElement, instance => setAttributes(instance), HTMLImageElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'decoding', 'decoding', 'async',
+ 'sync', 'decoding on HTMLImageElement', 'img',
+ getParentElement, instance => setAttributes(instance), HTMLImageElement
+);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLInputElement.html b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLInputElement.html
new file mode 100644
index 0000000000..adf43ee74d
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLInputElement.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Custom Elements: CEReactions on HTMLInputElement interface</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<link rel="author" title="Wanming Lin" href="mailto:wanming.lin@intel.com">
+<meta name="assert" content="capture of HTMLInputElement interface must have CEReactions">
+<meta name="help" content="https://www.w3.org/TR/html-media-capture/#the-capture-attribute">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/custom-elements-helpers.js"></script>
+<script src="../resources/reactions.js"></script>
+<body>
+<script>
+if ('capture' in HTMLInputElement.prototype) {
+ test(() => {
+ const element = define_build_in_custom_element(['capture'], HTMLInputElement, 'input');
+ const instance = document.createElement('input', { is: element.name });
+
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ instance['capture'] = 'user';
+ const logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'capture', oldValue: null, newValue: 'user', namespace: null});
+ }, 'capture on HTMLInputElement must enqueue an attributeChanged reaction when adding new attribute');
+
+ test(() => {
+ const element = define_build_in_custom_element(['capture'], HTMLInputElement, 'input');
+ const instance = document.createElement('input', { is: element.name });
+
+ instance['capture'] = 'user';
+ assert_array_equals(element.takeLog().types(), ['constructed', 'attributeChanged']);
+ instance['capture'] = 'environment';
+ const logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'capture', oldValue: 'user', newValue: 'environment', namespace: null});
+ }, 'capture on HTMLInputElement must enqueue an attributeChanged reaction when replacing an existing attribute');
+
+ test(() => {
+ const element = define_build_in_custom_element(['capture'], HTMLInputElement, 'input');
+ const instance = document.createElement('input', { is: element.name });
+
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ instance['capture'] = 'asdf';
+ const logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'capture', oldValue: null, newValue: 'asdf', namespace: null});
+ }, 'capture on HTMLInputElement must enqueue an attributeChanged reaction when adding invalid value default');
+
+ test(() => {
+ const element = define_build_in_custom_element(['capture'], HTMLInputElement, 'input');
+ const instance = document.createElement('input', { is: element.name });
+
+ instance['capture'] = 'user';
+ assert_array_equals(element.takeLog().types(), ['constructed', 'attributeChanged']);
+ instance['capture'] = '';
+ const logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'capture', oldValue: 'user', newValue: '', namespace: null});
+ }, 'capture on HTMLInputElement must enqueue an attributeChanged reaction when removing the attribute');
+} else {
+ // testharness.js doesn't allow a test file with no tests.
+ test(() => {
+ }, 'No tests if HTMLInputEement has no "capture" IDL attribute');
+}
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLLIElement.html b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLLIElement.html
new file mode 100644
index 0000000000..adba2addf6
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLLIElement.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<title>Custom Elements: CEReactions on HTMLLIElement interface</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<meta name="assert"
+ content="value of HTMLLIElement interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#the-li-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/custom-elements-helpers.js"></script>
+<script src="../resources/reactions.js"></script>
+
+<body>
+<script>
+
+function getParentElement(parentElementName) {
+ let parentElement = document.createElement(parentElementName);
+ document.body.appendChild(parentElement);
+ return parentElement;
+}
+
+testReflectAttributeWithParentNode(
+ 'value', 'value', '3',
+ '5', 'value on HTMLLIElement in ol', 'li',
+ () => getParentElement('ol'), HTMLLIElement
+);
+testReflectAttributeWithParentNode(
+ 'value', 'value', '3',
+ '5', 'value on HTMLLIElement in ul', 'li',
+ () => getParentElement('ul'), HTMLLIElement
+);
+testReflectAttributeWithParentNode(
+ 'value', 'value', '3',
+ '5', 'value on HTMLLIElement in menu', 'li',
+ () => getParentElement('menu'), HTMLLIElement
+);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLLabelElement.html b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLLabelElement.html
new file mode 100644
index 0000000000..2fe2741dad
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLLabelElement.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<title>Custom Elements: CEReactions on HTMLLabelElement interface</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<meta name="assert"
+ content="htmlFor of HTMLLabelElement interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#the-label-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/custom-elements-helpers.js"></script>
+<script src="../resources/reactions.js"></script>
+
+<form id="form">
+ <input type="radio" name="gender" id="male" value="male">
+ <input type="radio" name="gender" id="female" value="female">
+</form>
+<script>
+
+function getParentElement() {
+ let parentElement = document.getElementById("form");
+ return parentElement;
+}
+
+testReflectAttributeWithParentNode(
+ 'htmlFor', 'for', 'male',
+ 'female', 'htmlFor on HTMLLabelElement', 'label',
+ getParentElement, HTMLLabelElement
+);
+
+</script>
diff --git a/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLMapElement.html b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLMapElement.html
new file mode 100644
index 0000000000..5b2e674afb
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLMapElement.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<title>Custom Elements: CEReactions on HTMLMapElement interface</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<meta name="assert" content="name of HTMLMapElement interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#the-map-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/custom-elements-helpers.js"></script>
+<script src="../resources/reactions.js"></script>
+
+<img usemap="#yellow" src="/images/yellow.png" alt="yellow pic">
+<img usemap="#green" src="/images/green.png" alt="green pic">
+<script>
+
+testReflectAttribute('name', 'name', 'yellow', 'green', 'name on HTMLMapElement', 'map', HTMLMapElement);
+
+</script>
diff --git a/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLMediaElement.html b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLMediaElement.html
new file mode 100644
index 0000000000..58e002c52c
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLMediaElement.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<title>Custom Elements: CEReactions on HTMLMediaElement interface</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<meta name="assert" content="src, crossOrigin, preload, autoplay, loop,
+ controls, defaultMuted of HTMLMediaElement interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#media-elements">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/custom-elements-helpers.js"></script>
+<script src="../resources/reactions.js"></script>
+
+<body>
+<script>
+
+function getParentElement() {
+ return document.body;
+}
+
+function setAttributes(instance, value) {
+ instance.setAttribute('src', value);
+}
+
+testReflectAttribute(
+ 'src', 'src', '/media/sound_0.mp3',
+ '/media/sound_5.mp3', 'src on HTMLMediaElement in audio', 'audio',
+ HTMLAudioElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'crossOrigin', 'crossorigin', 'use-credentials',
+ 'anonymous', 'crossOrigin on HTMLMediaElement in audio', 'audio',
+ getParentElement, instance => setAttributes(instance, '/media/sound_5.mp3'),
+ HTMLAudioElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'preload', 'preload', 'auto',
+ 'none', 'preload on HTMLMediaElement in audio', 'audio',
+ getParentElement, instance => setAttributes(instance, '/media/sound_5.mp3'),
+ HTMLAudioElement
+);
+testReflectBooleanAttributeWithDependentAttributes(
+ 'autoplay', 'autoplay', 'autoplay on HTMLMediaElement in audio',
+ 'audio', getParentElement,
+ instance => setAttributes(instance, '/media/sound_5.mp3'),
+ HTMLAudioElement
+);
+testReflectBooleanAttributeWithDependentAttributes(
+ 'loop', 'loop', 'loop on HTMLMediaElement in audio',
+ 'audio', getParentElement,
+ instance => setAttributes(instance, '/media/sound_5.mp3'), HTMLAudioElement
+);
+testReflectBooleanAttributeWithDependentAttributes(
+ 'controls', 'controls', 'controls on HTMLMediaElement in audio',
+ 'audio', getParentElement,
+ instance => setAttributes(instance, '/media/sound_5.mp3'),
+ HTMLAudioElement
+);
+testReflectBooleanAttributeWithDependentAttributes(
+ 'defaultMuted', 'muted', 'defaultMuted on HTMLMediaElement in audio',
+ 'audio', getParentElement,
+ instance => setAttributes(instance, '/media/sound_5.mp3'),
+ HTMLAudioElement
+);
+
+testReflectAttribute(
+ 'src', 'src', '/media/video.ogv',
+ '/media/movie_5.mp4', 'src on HTMLMediaElement in video', 'video',
+ HTMLVideoElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'crossOrigin', 'crossorigin', 'use-credentials',
+ 'anonymous', 'crossOrigin on HTMLMediaElement in video', 'video',
+ getParentElement, instance => setAttributes(instance, '/media/movie_5.mp4'),
+ HTMLVideoElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'preload', 'preload', 'auto',
+ 'none', 'preload on HTMLMediaElement in video', 'video',
+ getParentElement, instance => setAttributes(instance, '/media/movie_5.mp4'),
+ HTMLVideoElement
+);
+testReflectBooleanAttributeWithDependentAttributes(
+ 'autoplay', 'autoplay', 'autoplay on HTMLMediaElement in video',
+ 'video', getParentElement,
+ instance => setAttributes(instance, '/media/movie_5.mp4'),
+ HTMLVideoElement
+);
+testReflectBooleanAttributeWithDependentAttributes(
+ 'loop', 'loop', 'loop on HTMLMediaElement in video',
+ 'video', getParentElement,
+ instance => setAttributes(instance, '/media/movie_5.mp4'),
+ HTMLVideoElement
+);
+testReflectBooleanAttributeWithDependentAttributes(
+ 'controls', 'controls', 'controls on HTMLMediaElement in video',
+ 'video', getParentElement,
+ instance => setAttributes(instance, '/media/movie_5.mp4'),
+ HTMLVideoElement
+);
+testReflectBooleanAttributeWithDependentAttributes(
+ 'defaultMuted', 'muted', 'defaultMuted on HTMLMediaElement in video',
+ 'video', getParentElement,
+ instance => setAttributes(instance, '/media/movie_5.mp4'),
+ HTMLVideoElement
+);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLMetaElement.html b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLMetaElement.html
new file mode 100644
index 0000000000..b6e8c06546
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLMetaElement.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<title>Custom Elements: CEReactions on HTMLMetaElement interface</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<meta name="assert" content="name, httpEquiv, content of
+ HTMLMetaElement interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#the-meta-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/custom-elements-helpers.js"></script>
+<script src="../resources/reactions.js"></script>
+
+<script>
+
+function getParentElement() {
+ return document.head;
+}
+
+function setAttributes(instance, attribute, value) {
+ instance.setAttribute(attribute, value);
+}
+
+testReflectAttributeWithDependentAttributes(
+ 'name', 'name', 'description',
+ 'keywords', 'name on HTMLMetaElement', 'meta',
+ getParentElement,
+ instance => setAttributes(instance, 'content', 'HTMLMetaElement'),
+ HTMLMetaElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'content', 'content', 'name1',
+ 'name2', 'content on HTMLMetaElement', 'meta',
+ getParentElement, instance => setAttributes(instance, 'name', 'author'),
+ HTMLMetaElement
+);
+
+test(() => {
+ let element = define_build_in_custom_element(
+ ['http-equiv'], HTMLMetaElement, 'meta'
+ );
+ let instance = document.createElement('meta', { is: element.name });
+
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ document.head.appendChild(instance);
+ assert_array_equals(element.takeLog().types(), ['connected']);
+ instance['content'] = '300';
+ instance['httpEquiv'] = 'refresh';
+ let logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {
+ name: 'http-equiv', oldValue: null, newValue: 'refresh', namespace: null
+ });
+}, 'httpEquiv on HTMLMetaElement must enqueue an attributeChanged'
+ + ' reaction when adding a new attribute');
+
+test(() => {
+ let element = define_build_in_custom_element(
+ ['http-equiv'], HTMLMetaElement, 'meta'
+ );
+ let instance = document.createElement('meta', { is: element.name });
+ document.head.appendChild(instance);
+
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+ instance['content'] = 'text/html; charset=UTF-8';
+ instance['httpEquiv'] = 'content-type';
+ assert_array_equals(element.takeLog().types(), ['attributeChanged']);
+ instance['content'] = '300';
+ instance['httpEquiv'] = 'refresh';
+ let logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {
+ name: 'http-equiv', oldValue: 'content-type',
+ newValue: 'refresh', namespace: null
+ });
+}, 'httpEquiv on HTMLMetaElement must enqueue an attributeChanged'
+ + ' reaction when replacing an existing attribute');
+
+</script>
diff --git a/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLMeterElement.html b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLMeterElement.html
new file mode 100644
index 0000000000..707e56c605
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLMeterElement.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<title>Custom Elements: CEReactions on HTMLMeterElement interface</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<meta name="assert" content="value, min, max, low, high, optimum of
+ HTMLMeterElement interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#the-meter-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/custom-elements-helpers.js"></script>
+<script src="../resources/reactions.js"></script>
+
+<body>
+<script>
+
+function getParentElement() {
+ return document.body;
+}
+
+function setAttributes(instance) {
+ instance.setAttribute('value', '0.6');
+}
+
+testReflectAttribute(
+ 'value', 'value', '0.3',
+ '0.4', 'value on HTMLMeterElement', 'meter',
+ HTMLMeterElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'min', 'min', '0.1',
+ '0.2', 'min on HTMLMeterElement', 'meter',
+ getParentElement, instance => setAttributes(instance), HTMLMeterElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'max', 'max', '2',
+ '3', 'max on HTMLMeterElement', 'meter',
+ getParentElement, instance => setAttributes(instance), HTMLMeterElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'low', 'low', '0.1',
+ '0.2', 'low on HTMLMeterElement', 'meter',
+ getParentElement, instance => setAttributes(instance), HTMLMeterElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'high', 'high', '2',
+ '3', 'high on HTMLMeterElement', 'meter',
+ getParentElement, instance => setAttributes(instance), HTMLMeterElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'optimum', 'optimum', '0.3',
+ '0.4', 'optimum on HTMLMeterElement', 'meter',
+ getParentElement, instance => setAttributes(instance), HTMLMeterElement
+);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLModElement.html b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLModElement.html
new file mode 100644
index 0000000000..850fe170a5
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLModElement.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<title>Custom Elements: CEReactions on HTMLModElement interface</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<meta name="assert" content="cite, dateTime of HTMLModElement interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#attributes-common-to-ins-and-del-elements">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<script src="./resources/reactions.js"></script>
+
+<script>
+
+testReflectAttribute('cite', 'cite', '../resources/custom-elements-helpers.js', './resources/reactions.js', 'cite on ins use HTMLModElement', 'ins', HTMLModElement);
+testReflectAttribute('dateTime', 'datetime', '2018-12-19 00:00Z', '2018-12-20 00:00Z', 'dateTime on ins use HTMLModElement', 'ins', HTMLModElement);
+testReflectAttribute('cite', 'cite', '../resources/custom-elements-helpers.js', './resources/reactions.js', 'cite on del use HTMLModElement', 'del', HTMLModElement);
+testReflectAttribute('dateTime', 'datetime', '2018-10-11T01:25-07:00', '2018-10-12T01:25-07:00', 'dateTime on del use HTMLModElement', 'del', HTMLModElement);
+
+</script>
diff --git a/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLOListElement.html b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLOListElement.html
new file mode 100644
index 0000000000..b62f31b489
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLOListElement.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<title>Custom Elements: CEReactions on HTMLOListElement interface</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<meta name="assert" content="reversed, start, type of HTMLOListElement interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#the-ol-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/custom-elements-helpers.js"></script>
+<script src="../resources/reactions.js"></script>
+
+<script>
+
+testReflectBooleanAttribute('reversed', 'reversed', 'reversed on HTMLOListElement', 'ol', HTMLOListElement);
+testReflectAttribute('start', 'start', '2', '5', 'start on HTMLOListElement', 'ol', HTMLOListElement);
+testReflectAttribute('type', 'type', '1', 'a', 'type on HTMLOListElement', 'ol', HTMLOListElement);
+
+</script>
diff --git a/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLOptGroupElement.html b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLOptGroupElement.html
new file mode 100644
index 0000000000..afa31bb465
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLOptGroupElement.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<title>Custom Elements: CEReactions on HTMLOptGroupElement interface</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<meta name="assert" content="disabled, label of
+ HTMLOptGroupElement interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#the-optgroup-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<script src="./resources/reactions.js"></script>
+
+<body>
+<script>
+
+function getParentElement() {
+ let element = document.createElement('select');
+ document.body.appendChild(element);
+ return element;
+}
+
+function setAttributes(instance) {
+ instance.setAttribute('label', 'group1');
+}
+
+testReflectBooleanAttributeWithDependentAttributes(
+ 'disabled', 'disabled', 'disabled on HTMLOptGroupElement',
+ 'optgroup', getParentElement, instance => setAttributes(instance),
+ HTMLOptGroupElement
+);
+
+testReflectAttributeWithParentNode(
+ 'label', 'label', 'group1',
+ 'group2', 'label on HTMLOptGroupElement', 'optgroup',
+ getParentElement, HTMLOptGroupElement
+);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLParamElement.html b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLParamElement.html
new file mode 100644
index 0000000000..eb3a13962f
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLParamElement.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<title>Custom Elements: CEReactions on HTMLParamElement interface</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<meta name="assert" content="name, value of HTMLParamElement
+ interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#the-param-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/custom-elements-helpers.js"></script>
+<script src="../resources/reactions.js"></script>
+
+<body>
+<script>
+
+function getParentElement() {
+ let element = document.createElement('object');
+ element['type'] = 'image/png';
+ element['data'] = '/images/blue.png';
+ document.body.appendChild(element);
+ return element;
+}
+
+function setAttributes(instance, attribute, value) {
+ instance.setAttribute(attribute, value);
+}
+
+testReflectAttributeWithDependentAttributes(
+ 'name', 'name', 'image1',
+ 'image2', 'name on HTMLParamElement', 'param',
+ getParentElement, instance => setAttributes(instance, 'value', 'blue'),
+ HTMLParamElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'value', 'value', 'blue1',
+ 'blue2', 'value on HTMLParamElement', 'param',
+ getParentElement, instance => setAttributes(instance, 'name', 'image'),
+ HTMLParamElement
+);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLProgressElement.html b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLProgressElement.html
new file mode 100644
index 0000000000..42683f4ede
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLProgressElement.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>Custom Elements: CEReactions on HTMLProgressElement interface</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<meta name="assert" content="value, max of HTMLProgressElement
+ interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#the-progress-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/custom-elements-helpers.js"></script>
+<script src="../resources/reactions.js"></script>
+
+<script>
+
+testReflectAttribute(
+ 'value', 'value', '0.15',
+ '0.2', 'value on HTMLProgressElement', 'progress',
+ HTMLProgressElement
+);
+testReflectAttribute(
+ 'max', 'max', '2',
+ '4', 'max on HTMLProgressElement', 'progress',
+ HTMLProgressElement
+);
+
+</script>
diff --git a/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLQuoteElement.html b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLQuoteElement.html
new file mode 100644
index 0000000000..f9c3d7538c
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLQuoteElement.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<title>Custom Elements: CEReactions on HTMLQuoteElement interface</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<meta name="assert" content="cite of HTMLQuoteElement interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#the-blockquote-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/custom-elements-helpers.js"></script>
+<script src="../resources/reactions.js"></script>
+
+<script>
+
+testReflectAttribute('cite', 'cite', '../resources/custom-elements-helpers.js', './resources/reactions.js', 'cite on blockquote use HTMLQuoteElement', 'blockquote', HTMLQuoteElement);
+testReflectAttribute('cite', 'cite', '../resources/custom-elements-helpers.js', './resources/reactions.js', 'cite on q use HTMLQuoteElement', 'q', HTMLQuoteElement);
+
+</script>
diff --git a/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLSlotElement.html b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLSlotElement.html
new file mode 100644
index 0000000000..56871873b4
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLSlotElement.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<title>Custom Elements: CEReactions on HTMLSlotElement interface</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<meta name="assert" content="name of HTMLSlotElement interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#the-slot-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/custom-elements-helpers.js"></script>
+<script src="../resources/reactions.js"></script>
+
+<script>
+
+testReflectAttribute('name', 'name', 'slot1', 'slot2', 'name on HTMLSlotElement', 'slot', HTMLSlotElement);
+
+</script>
diff --git a/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLSourceElement.html b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLSourceElement.html
new file mode 100644
index 0000000000..f7d567ebcb
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLSourceElement.html
@@ -0,0 +1,191 @@
+<!DOCTYPE html>
+<title>Custom Elements: CEReactions on HTMLSourceElement interface</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<meta name="assert" content="src, type, srcset, sizes, media of
+ HTMLSourceElement interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#the-source-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/custom-elements-helpers.js"></script>
+<script src="../resources/reactions.js"></script>
+
+<video controls id='video' width='5' height='5'></video>
+<picture id='pic'>
+ <img src='/images/green-1x1.png'>
+</picture>
+<body>
+<script>
+
+function getParentElement(id) {
+ let element = document.getElementById(id);
+ return element;
+}
+
+testReflectAttributeWithParentNode(
+ 'src', 'src', '/media/video.ogv',
+ '/media/white.mp4', 'src on HTMLSourceElement', 'source',
+ () => getParentElement('video'), HTMLSourceElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'type', 'type', 'video/mp4; codecs="mp4v.20.240, mp4a.40.2"',
+ 'video/mp4; codecs="mp4v.20.8, mp4a.40.2"', 'type on HTMLSourceElement',
+ 'source',
+ () => getParentElement('video'),
+ instance => instance.setAttribute('src', '/media/white.mp4'), HTMLSourceElement
+);
+
+function testReflectAttributeWithContentValuesAndParentNode(
+ jsAttributeName, contentAttributeName, validValue1,
+ contentValue1, validValue2, contentValue2,
+ name, elementName, getParentElement,
+ interfaceName) {
+
+ let parentElement = getParentElement();
+
+ test(() => {
+ let element = define_build_in_custom_element(
+ [contentAttributeName], interfaceName, elementName
+ );
+ let instance = document.createElement(elementName, { is: element.name });
+
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ // source element as a child of a picture element, before the img element
+ parentElement.prepend(instance);
+ assert_array_equals(element.takeLog().types(), ['connected']);
+ instance[jsAttributeName] = validValue1;
+ let logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(
+ logEntries.last(),
+ { name: contentAttributeName, oldValue: null,
+ newValue: contentValue1, namespace: null
+ }
+ );
+ }, name + ' must enqueue an attributeChanged reaction when adding a new attribute');
+
+ test(() => {
+ let element = define_build_in_custom_element(
+ [contentAttributeName], interfaceName, elementName
+ );
+ let instance = document.createElement(elementName, { is: element.name });
+ // source element as a child of a picture element, before the img element
+ parentElement.prepend(instance);
+
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+ instance[jsAttributeName] = validValue1;
+ assert_array_equals(element.takeLog().types(), ['attributeChanged']);
+ instance[jsAttributeName] = validValue2;
+ let logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(
+ logEntries.last(),
+ { name: contentAttributeName, oldValue: contentValue1,
+ newValue: contentValue2, namespace: null
+ }
+ );
+ }, name + ' must enqueue an attributeChanged reaction when replacing an existing attribute');
+}
+
+function testReflectAttributeWithParentNode(
+ jsAttributeName, contentAttributeName, validValue1,
+ validValue2, name, elementName,
+ getParentElement, interfaceName) {
+
+ testReflectAttributeWithContentValuesAndParentNode(
+ jsAttributeName, contentAttributeName, validValue1,
+ validValue1, validValue2, validValue2,
+ name, elementName, getParentElement,
+ interfaceName
+ );
+}
+
+function testReflectAttributeWithContentValuesAndDependentAttributes(
+ jsAttributeName, contentAttributeName, validValue1,
+ contentValue1, validValue2, contentValue2,
+ name, elementName, getParentElement,
+ setAttributes, interfaceName) {
+
+ let parentElement = getParentElement();
+
+ test(() => {
+ let element = define_build_in_custom_element(
+ [contentAttributeName], interfaceName, elementName
+ );
+ let instance = document.createElement(elementName, { is: element.name });
+
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ // source element as a child of a picture element, before the img element
+ parentElement.prepend(instance);
+ assert_array_equals(element.takeLog().types(), ['connected']);
+ setAttributes(instance);
+ instance[jsAttributeName] = validValue1;
+ let logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(
+ logEntries.last(),
+ { name: contentAttributeName, oldValue: null,
+ newValue: contentValue1, namespace: null
+ }
+ );
+ }, name + ' must enqueue an attributeChanged reaction when adding a new attribute');
+
+ test(() => {
+ let element = define_build_in_custom_element(
+ [contentAttributeName], interfaceName, elementName
+ );
+ let instance = document.createElement(elementName, { is: element.name });
+ // source element as a child of a picture element, before the img element
+ parentElement.prepend(instance);
+ setAttributes(instance);
+ instance[jsAttributeName] = validValue1;
+
+ assert_array_equals(
+ element.takeLog().types(),
+ ['constructed', 'connected', 'attributeChanged']
+ );
+ instance[jsAttributeName] = validValue2;
+ let logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(
+ logEntries.last(),
+ { name: contentAttributeName, oldValue: contentValue1,
+ newValue: contentValue2, namespace: null
+ }
+ );
+ }, name + ' must enqueue an attributeChanged reaction when replacing an existing attribute');
+}
+
+function testReflectAttributeWithDependentAttributes(
+ jsAttributeName, contentAttributeName, validValue1,
+ validValue2, name, elementName,
+ getParentElement, setAttributes, interfaceName) {
+
+ testReflectAttributeWithContentValuesAndDependentAttributes(
+ jsAttributeName, contentAttributeName, validValue1,
+ validValue1, validValue2, validValue2,
+ name, elementName, getParentElement,
+ setAttributes, interfaceName);
+}
+
+testReflectAttributeWithParentNode(
+ 'srcset', 'srcset', '/images/green.png',
+ '/images/green-1x1.png', 'srcset on HTMLSourceElement', 'source',
+ () => getParentElement('pic'), HTMLSourceElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'sizes', 'sizes', '(max-width: 32px) 28px',
+ '(max-width: 48px) 44px', 'sizes on HTMLSourceElement', 'source',
+ () => getParentElement('pic'),
+ instance => instance.setAttribute('srcset', '/images/green.png 3x'),
+ HTMLSourceElement
+);
+testReflectAttributeWithDependentAttributes(
+ 'media', 'media', '(max-width: 7px)',
+ '(max-width: 9px)', 'media on HTMLSourceElement', 'source',
+ () => getParentElement('pic'),
+ instance => instance.setAttribute('srcset', '/images/green.png 3x'),
+ HTMLSourceElement
+);
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLStyleElement.html b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLStyleElement.html
new file mode 100644
index 0000000000..d68d5cb76d
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLStyleElement.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<title>Custom Elements: CEReactions on HTMLStyleElement interface</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<meta name="assert" content="media of HTMLStyleElement interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#the-style-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/custom-elements-helpers.js"></script>
+<script src="../resources/reactions.js"></script>
+
+<script>
+
+function getParentElement() {
+ return document.head;
+}
+
+testReflectAttributeWithParentNode(
+ 'media', 'media', 'print',
+ 'screen', 'media on HTMLStyleElement', 'style',
+ getParentElement, HTMLStyleElement
+);
+
+</script>
diff --git a/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLTableCellElement.html b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLTableCellElement.html
new file mode 100644
index 0000000000..95a8459df8
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLTableCellElement.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<title>Custom Elements: CEReactions on HTMLTableCellElement interface</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<meta name="assert" content="colSpan, rowSpan, headers, scope, abbr of
+ HTMLTableCellElement interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#the-td-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/custom-elements-helpers.js"></script>
+<script src="../resources/reactions.js"></script>
+
+<table><tr id="colSpan"></table>
+<table><tr id="rowSpan"><tr><tr></table>
+<table><tr><th id="id1"><th id="id2"><tr id="td_headers"><tr id="th_headers"></table>
+<script>
+
+function getParentElement(id) {
+ let parentElement = document.getElementById(id);
+ return parentElement;
+}
+
+testReflectAttributeWithParentNode(
+ 'colSpan', 'colspan', '2',
+ '3', 'colSpan on HTMLTableCellElement in td', 'td',
+ () => getParentElement('colSpan'), HTMLTableCellElement
+);
+testReflectAttributeWithParentNode(
+ 'colSpan', 'colspan', '2',
+ '3', 'colSpan on HTMLTableCellElement in th', 'th',
+ () => getParentElement('colSpan'), HTMLTableCellElement
+);
+testReflectAttributeWithParentNode(
+ 'rowSpan', 'rowspan', '2',
+ '3', 'rowSpan on HTMLTableCellElement in td', 'td',
+ () => getParentElement('rowSpan'), HTMLTableCellElement
+);
+testReflectAttributeWithParentNode(
+ 'rowSpan', 'rowspan', '2',
+ '3', 'rowSpan on HTMLTableCellElement in th', 'th',
+ () => getParentElement('rowSpan'), HTMLTableCellElement
+);
+testReflectAttributeWithParentNode(
+ 'headers', 'headers', 'id1',
+ 'id2', 'headers on HTMLTableCellElement in td', 'td',
+ () => getParentElement('td_headers'), HTMLTableCellElement
+);
+testReflectAttributeWithParentNode(
+ 'headers', 'headers', 'id1',
+ 'id2', 'headers on HTMLTableCellElement in th', 'th',
+ () => getParentElement('th_headers'), HTMLTableCellElement
+);
+testReflectAttributeWithParentNode(
+ 'scope', 'scope', 'row',
+ 'col', 'scope on HTMLTableCellElement in th', 'th',
+ () => getParentElement('colSpan'), HTMLTableCellElement
+);
+testReflectAttributeWithParentNode(
+ 'abbr', 'abbr', 'Model1',
+ 'Model2', 'abbr on HTMLTableCellElement in th', 'th',
+ () => getParentElement('colSpan'), HTMLTableCellElement
+);
+
+</script>
diff --git a/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLTableColElement.html b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLTableColElement.html
new file mode 100644
index 0000000000..8e4d1359d8
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLTableColElement.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>Custom Elements: CEReactions on HTMLTableColElement interface</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<meta name="assert" content="span of HTMLTableColElement interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#the-colgroup-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/custom-elements-helpers.js"></script>
+<script src="../resources/reactions.js"></script>
+
+<table id="tbl"></table>
+<script>
+
+function getParentElement() {
+ let parentElement = document.getElementById('tbl');
+ return parentElement;
+}
+
+testReflectAttributeWithParentNode(
+ 'span', 'span', '2',
+ '1', 'span on HTMLTableColElement', 'colgroup',
+ getParentElement, HTMLTableColElement
+);
+
+</script>
diff --git a/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLTimeElement.html b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLTimeElement.html
new file mode 100644
index 0000000000..b2f4cc8af7
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/customized-builtins/HTMLTimeElement.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<title>Custom Elements: CEReactions on HTMLTimeElement interface</title>
+<link rel="author" title="Intel" href="http://www.intel.com">
+<meta name="assert" content="name of HTMLTimeElement interface must have CEReactions">
+<meta name="help" content="https://html.spec.whatwg.org/#the-time-element">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../resources/custom-elements-helpers.js"></script>
+<script src="../resources/reactions.js"></script>
+
+<script>
+
+testReflectAttribute('dateTime', 'datetime', '2018-12-10', '2018-12-12', 'dateTime on HTMLTimeElement', 'time', HTMLTimeElement);
+
+</script>
diff --git a/testing/web-platform/tests/custom-elements/reactions/resources/reactions.js b/testing/web-platform/tests/custom-elements/reactions/resources/reactions.js
new file mode 100644
index 0000000000..5ed32a4fa4
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/resources/reactions.js
@@ -0,0 +1,452 @@
+
+let testNumber = 1;
+
+function testNodeConnector(testFunction, name) {
+ let container = document.createElement('div');
+ container.appendChild(document.createElement('div'));
+ document.body.appendChild(container);
+
+ test(function () {
+ var element = define_new_custom_element();
+ var instance = document.createElement(element.name);
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ testFunction(container, instance);
+ assert_array_equals(element.takeLog().types(), ['connected']);
+ }, name + ' must enqueue a connected reaction');
+
+ test(function () {
+ var element = define_new_custom_element();
+ var instance = document.createElement(element.name);
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ var newDoc = document.implementation.createHTMLDocument();
+ testFunction(container, instance);
+ assert_array_equals(element.takeLog().types(), ['connected']);
+ testFunction(newDoc.documentElement, instance);
+ assert_array_equals(element.takeLog().types(), ['disconnected', 'adopted', 'connected']);
+ }, name + ' must enqueue a disconnected reaction, an adopted reaction, and a connected reaction when the custom element was in another document');
+
+ container.parentNode.removeChild(container);
+}
+
+function testNodeDisconnector(testFunction, name) {
+ let container = document.createElement('div');
+ container.appendChild(document.createElement('div'));
+ document.body.appendChild(container);
+
+ test(function () {
+ var element = define_new_custom_element();
+ var instance = document.createElement(element.name);
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ container.appendChild(instance);
+ assert_array_equals(element.takeLog().types(), ['connected']);
+ testFunction(instance, window);
+ assert_array_equals(element.takeLog().types(), ['disconnected']);
+ }, name + ' must enqueue a disconnected reaction');
+
+ container.parentNode.removeChild(container);
+}
+
+function testInsertingMarkup(testFunction, name) {
+ let container = document.createElement('div');
+ container.appendChild(document.createElement('div'));
+ document.body.appendChild(container);
+
+ test(function () {
+ var element = define_new_custom_element();
+ testFunction(container, `<${element.name}></${element.name}>`);
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+ }, name + ' must enqueue a connected reaction for a newly constructed custom element');
+
+ test(function () {
+ var element = define_new_custom_element(['title']);
+ testFunction(container, `<${element.name} id="hello" title="hi"></${element.name}>`);
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['constructed', 'attributeChanged', 'connected']);
+ assert_attribute_log_entry(logEntries[1], {name: 'title', oldValue: null, newValue: 'hi', namespace: null});
+ }, name + ' must enqueue a attributeChanged reaction for a newly constructed custom element');
+
+ container.parentNode.removeChild(container);
+}
+
+function testParsingMarkup(testFunction, name) {
+ test(function () {
+ var element = define_new_custom_element(['id']);
+ assert_array_equals(element.takeLog().types(), []);
+ var instance = testFunction(document, `<${element.name} id="hello" class="foo"></${element.name}>`);
+ assert_equals(Object.getPrototypeOf(instance.querySelector(element.name)), element.class.prototype);
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['constructed', 'attributeChanged']);
+ assert_attribute_log_entry(logEntries[1], {name: 'id', oldValue: null, newValue: 'hello', namespace: null});
+ }, name + ' must construct a custom element');
+}
+
+function testCloner(testFunction, name) {
+ let container = document.createElement('div');
+ container.appendChild(document.createElement('div'));
+ document.body.appendChild(container);
+
+ test(function () {
+ var element = define_new_custom_element(['id']);
+ var instance = document.createElement(element.name);
+ container.appendChild(instance);
+
+ instance.setAttribute('id', 'foo');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected', 'attributeChanged']);
+ var newInstance = testFunction(instance);
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['constructed', 'attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'id', oldValue: null, newValue: 'foo', namespace: null});
+ }, name + ' must enqueue an attributeChanged reaction when cloning an element with an observed attribute');
+
+ test(function () {
+ var element = define_new_custom_element(['id']);
+ var instance = document.createElement(element.name);
+ container.appendChild(instance);
+
+ instance.setAttribute('lang', 'en');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+ var newInstance = testFunction(instance);
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ }, name + ' must not enqueue an attributeChanged reaction when cloning an element with an unobserved attribute');
+
+ test(function () {
+ var element = define_new_custom_element(['title', 'class']);
+ var instance = document.createElement(element.name);
+ container.appendChild(instance);
+
+ instance.setAttribute('lang', 'en');
+ instance.className = 'foo';
+ instance.setAttribute('title', 'hello world');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected', 'attributeChanged', 'attributeChanged']);
+ var newInstance = testFunction(instance);
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['constructed', 'attributeChanged', 'attributeChanged']);
+ assert_attribute_log_entry(logEntries[1], {name: 'class', oldValue: null, newValue: 'foo', namespace: null});
+ assert_attribute_log_entry(logEntries[2], {name: 'title', oldValue: null, newValue: 'hello world', namespace: null});
+ }, name + ' must enqueue an attributeChanged reaction when cloning an element only for observed attributes');
+}
+
+function testReflectAttributeWithContentValues(jsAttributeName, contentAttributeName, validValue1, contentValue1, validValue2, contentValue2, name, elementName, interfaceName) {
+ test(function () {
+ if (elementName === undefined) {
+ var element = define_new_custom_element([contentAttributeName]);
+ var instance = document.createElement(element.name);
+ } else {
+ var element = define_build_in_custom_element([contentAttributeName], interfaceName, elementName);
+ var instance = document.createElement(elementName, { is: element.name });
+ }
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ instance[jsAttributeName] = validValue1;
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+
+ assert_attribute_log_entry(logEntries.last(), {name: contentAttributeName, oldValue: null, newValue: contentValue1, namespace: null});
+ }, name + ' must enqueue an attributeChanged reaction when adding ' + contentAttributeName + ' content attribute');
+
+ test(function () {
+ if (elementName === undefined) {
+ var element = define_new_custom_element([contentAttributeName]);
+ var instance = document.createElement(element.name);
+ } else {
+ var element = define_build_in_custom_element([contentAttributeName], interfaceName, elementName);
+ var instance = document.createElement(elementName, { is: element.name });
+ }
+ instance[jsAttributeName] = validValue1;
+ assert_array_equals(element.takeLog().types(), ['constructed', 'attributeChanged']);
+ instance[jsAttributeName] = validValue2;
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: contentAttributeName, oldValue: contentValue1, newValue: contentValue2, namespace: null});
+ }, name + ' must enqueue an attributeChanged reaction when replacing an existing attribute');
+}
+
+function testReflectAttribute(jsAttributeName, contentAttributeName, validValue1, validValue2, name, elementName, interfaceName) {
+ testReflectAttributeWithContentValues(jsAttributeName, contentAttributeName, validValue1, validValue1, validValue2, validValue2, name, elementName, interfaceName);
+}
+
+function testReflectBooleanAttribute(jsAttributeName, contentAttributeName, name, elementName, interfaceName) {
+ testReflectAttributeWithContentValues(jsAttributeName, contentAttributeName, true, '', false, null, name, elementName, interfaceName);
+}
+
+function testReflectAttributeWithContentValuesAndDependentAttributes(jsAttributeName, contentAttributeName, validValue1, contentValue1, validValue2, contentValue2, name, elementName, getParentElement, setAttributes, interfaceName) {
+ let parentElement = getParentElement();
+
+ test(() => {
+ let element = define_build_in_custom_element([contentAttributeName], interfaceName, elementName);
+ let instance = document.createElement(elementName, { is: element.name });
+
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ parentElement.appendChild(instance);
+ assert_array_equals(element.takeLog().types(), ['connected']);
+ setAttributes(instance);
+ instance[jsAttributeName] = validValue1;
+ let logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), { name: contentAttributeName, oldValue: null, newValue: contentValue1, namespace: null });
+
+ }, name + ' must enqueue an attributeChanged reaction when adding a new attribute');
+
+ test(() => {
+ let element = define_build_in_custom_element([contentAttributeName], interfaceName, elementName);
+ let instance = document.createElement(elementName, { is: element.name });
+ parentElement.appendChild(instance);
+ setAttributes(instance);
+ instance[jsAttributeName] = validValue1;
+
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected', 'attributeChanged']);
+ instance[jsAttributeName] = validValue2;
+ let logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), { name: contentAttributeName, oldValue: contentValue1, newValue: contentValue2, namespace: null });
+
+ }, name + ' must enqueue an attributeChanged reaction when replacing an existing attribute');
+}
+
+function testReflectAttributeWithDependentAttributes(jsAttributeName, contentAttributeName, validValue1, validValue2, name, elementName, getParentElement, setAttributes, interfaceName) {
+ testReflectAttributeWithContentValuesAndDependentAttributes(jsAttributeName, contentAttributeName, validValue1, validValue1, validValue2, validValue2, name, elementName, getParentElement, setAttributes, interfaceName);
+}
+
+function testReflectBooleanAttributeWithDependentAttributes(jsAttributeName, contentAttributeName, name, elementName, getParentElement, setAttributes, interfaceName) {
+ testReflectAttributeWithContentValuesAndDependentAttributes(jsAttributeName, contentAttributeName, true, '', false, null, name, elementName, getParentElement, setAttributes, interfaceName);
+}
+
+function testReflectAttributeWithContentValuesAndParentNode(jsAttributeName, contentAttributeName, validValue1, contentValue1, validValue2, contentValue2, name, elementName, getParentElement, interfaceName) {
+ let parentElement = getParentElement();
+
+ test(() => {
+ let element = define_build_in_custom_element([contentAttributeName], interfaceName, elementName);
+ let instance = document.createElement(elementName, { is: element.name });
+
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ parentElement.appendChild(instance);
+ assert_array_equals(element.takeLog().types(), ['connected']);
+ instance[jsAttributeName] = validValue1;
+ let logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), { name: contentAttributeName, oldValue: null, newValue: contentValue1, namespace: null });
+}, name + ' must enqueue an attributeChanged reaction when adding a new attribute');
+
+ test(() => {
+ let element = define_build_in_custom_element([contentAttributeName], interfaceName, elementName);
+ let instance = document.createElement(elementName, { is: element.name });
+ parentElement.appendChild(instance);
+
+ assert_array_equals(element.takeLog().types(), ['constructed', 'connected']);
+ instance[jsAttributeName] = validValue1;
+ assert_array_equals(element.takeLog().types(), ['attributeChanged']);
+ instance[jsAttributeName] = validValue2;
+ let logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), { name: contentAttributeName, oldValue: contentValue1, newValue: contentValue2, namespace: null });
+ }, name + ' must enqueue an attributeChanged reaction when replacing an existing attribute');
+}
+
+function testReflectAttributeWithParentNode(jsAttributeName, contentAttributeName, validValue1, validValue2, name, elementName, getParentElement, interfaceName) {
+ testReflectAttributeWithContentValuesAndParentNode(jsAttributeName, contentAttributeName, validValue1, validValue1, validValue2, validValue2, name, elementName, getParentElement, interfaceName);
+}
+
+function testReflectBooleanAttributeWithParentNode(jsAttributeName, contentAttributeName, name, elementName, getParentElement, interfaceName) {
+ testReflectAttributeWithContentValuesAndParentNode(jsAttributeName, contentAttributeName, true, '', false, null, name, elementName, getParentElement, interfaceName);
+}
+
+function testAttributeAdder(testFunction, name) {
+ test(function () {
+ var element = define_new_custom_element(['id']);
+ var instance = document.createElement(element.name);
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ testFunction(instance, 'id', 'foo');
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'id', oldValue: null, newValue: 'foo', namespace: null});
+ }, name + ' must enqueue an attributeChanged reaction when adding an attribute');
+
+ test(function () {
+ var element = define_new_custom_element(['class']);
+ var instance = document.createElement(element.name);
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ testFunction(instance, 'data-lang', 'en');
+ assert_array_equals(element.takeLog().types(), []);
+ }, name + ' must not enqueue an attributeChanged reaction when adding an unobserved attribute');
+
+ test(function () {
+ var element = define_new_custom_element(['title']);
+ var instance = document.createElement(element.name);
+ instance.setAttribute('title', 'hello');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'attributeChanged']);
+ testFunction(instance, 'title', 'world');
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'title', oldValue: 'hello', newValue: 'world', namespace: null});
+ }, name + ' must enqueue an attributeChanged reaction when replacing an existing attribute');
+
+ test(function () {
+ var element = define_new_custom_element([]);
+ var instance = document.createElement(element.name);
+ instance.setAttribute('data-lang', 'zh');
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ testFunction(instance, 'data-lang', 'en');
+ assert_array_equals(element.takeLog().types(), []);
+ }, name + ' must enqueue an attributeChanged reaction when replacing an existing unobserved attribute');
+}
+
+function testAttributeMutator(testFunction, name) {
+ test(function () {
+ var element = define_new_custom_element(['title']);
+ var instance = document.createElement(element.name);
+ instance.setAttribute('title', 'hello');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'attributeChanged']);
+ testFunction(instance, 'title', 'world');
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'title', oldValue: 'hello', newValue: 'world', namespace: null});
+ }, name + ' must enqueue an attributeChanged reaction when replacing an existing attribute');
+
+ test(function () {
+ var element = define_new_custom_element(['class']);
+ var instance = document.createElement(element.name);
+ instance.setAttribute('data-lang', 'zh');
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ testFunction(instance, 'data-lang', 'en');
+ assert_array_equals(element.takeLog().types(), []);
+ }, name + ' must not enqueue an attributeChanged reaction when replacing an existing unobserved attribute');
+}
+
+function testAttributeRemover(testFunction, name, options) {
+ if (options && !options.onlyExistingAttribute) {
+ test(function () {
+ var element = define_new_custom_element(['title']);
+ var instance = document.createElement(element.name);
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ testFunction(instance, 'title');
+ assert_array_equals(element.takeLog().types(), []);
+ }, name + ' must not enqueue an attributeChanged reaction when removing an attribute that does not exist');
+ }
+
+ test(function () {
+ var element = define_new_custom_element([]);
+ var instance = document.createElement(element.name);
+ instance.setAttribute('data-lang', 'hello');
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ testFunction(instance, 'data-lang');
+ assert_array_equals(element.takeLog().types(), []);
+ }, name + ' must not enqueue an attributeChanged reaction when removing an unobserved attribute');
+
+ test(function () {
+ var element = define_new_custom_element(['title']);
+ var instance = document.createElement(element.name);
+ instance.setAttribute('title', 'hello');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'attributeChanged']);
+ testFunction(instance, 'title');
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'title', oldValue: 'hello', newValue: null, namespace: null});
+ }, name + ' must enqueue an attributeChanged reaction when removing an existing attribute');
+
+ test(function () {
+ var element = define_new_custom_element([]);
+ var instance = document.createElement(element.name);
+ instance.setAttribute('data-lang', 'ja');
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ testFunction(instance, 'data-lang');
+ assert_array_equals(element.takeLog().types(), []);
+ }, name + ' must not enqueue an attributeChanged reaction when removing an existing unobserved attribute');
+}
+
+function test_mutating_style_property_value(testFunction, name, options) {
+ const propertyName = (options || {}).propertyName || 'color';
+ const idlName = (options || {}).idlName || 'color';
+ const value1 = (options || {}).value1 || 'blue';
+ const rule1 = `${propertyName}: ${value1};`;
+ const value2 = (options || {}).value2 || 'red';
+ const rule2 = `${propertyName}: ${value2};`;
+
+ test(function () {
+ var element = define_new_custom_element(['style']);
+ var instance = document.createElement(element.name);
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ testFunction(instance, propertyName, idlName, value1);
+ assert_equals(instance.getAttribute('style'), rule1);
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'style', oldValue: null, newValue: rule1, namespace: null});
+ }, name + ' must enqueue an attributeChanged reaction when it adds the observed style attribute');
+
+ test(function () {
+ var element = define_new_custom_element(['title']);
+ var instance = document.createElement(element.name);
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ testFunction(instance, propertyName, idlName, value1);
+ assert_equals(instance.getAttribute('style'), rule1);
+ assert_array_equals(element.takeLog().types(), []);
+ }, name + ' must not enqueue an attributeChanged reaction when it adds the style attribute but the style attribute is not observed');
+
+ test(function () {
+ var element = define_new_custom_element(['style']);
+ var instance = document.createElement(element.name);
+ testFunction(instance, propertyName, idlName, value1);
+ assert_array_equals(element.takeLog().types(), ['constructed', 'attributeChanged']);
+ testFunction(instance, propertyName, idlName, value2);
+ assert_equals(instance.getAttribute('style'), rule2);
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'style', oldValue: rule1, newValue: rule2, namespace: null});
+ }, name + ' must enqueue an attributeChanged reaction when it mutates the observed style attribute');
+
+ test(function () {
+ var element = define_new_custom_element([]);
+ var instance = document.createElement(element.name);
+ testFunction(instance, propertyName, idlName, value1);
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ testFunction(instance, propertyName, idlName, value2);
+ assert_equals(instance.getAttribute('style'), rule2);
+ assert_array_equals(element.takeLog().types(), []);
+ }, name + ' must not enqueue an attributeChanged reaction when it mutates the style attribute but the style attribute is not observed');
+}
+
+function test_removing_style_property_value(testFunction, name) {
+ test(function () {
+ var element = define_new_custom_element(['style']);
+ var instance = document.createElement(element.name);
+ instance.setAttribute('style', 'color: red; display: none;');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'attributeChanged']);
+ testFunction(instance, 'color', 'color');
+ assert_equals(instance.getAttribute('style'), 'display: none;'); // Don't make this empty since browser behaviors are inconsistent now.
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'style', oldValue: 'color: red; display: none;', newValue: 'display: none;', namespace: null});
+ }, name + ' must enqueue an attributeChanged reaction when it removes a property from the observed style attribute');
+
+ test(function () {
+ var element = define_new_custom_element(['class']);
+ var instance = document.createElement(element.name);
+ instance.setAttribute('style', 'color: red; display: none;');
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ testFunction(instance, 'color', 'color');
+ assert_equals(instance.getAttribute('style'), 'display: none;'); // Don't make this empty since browser behaviors are inconsistent now.
+ assert_array_equals(element.takeLog().types(), []);
+ }, name + ' must not enqueue an attributeChanged reaction when it removes a property from the style attribute but the style attribute is not observed');
+}
+
+function test_mutating_style_property_priority(testFunction, name) {
+ test(function () {
+ var element = define_new_custom_element(['style']);
+ var instance = document.createElement(element.name);
+ instance.setAttribute('style', 'color: red');
+ assert_array_equals(element.takeLog().types(), ['constructed', 'attributeChanged']);
+ testFunction(instance, 'color', 'color', true);
+ assert_equals(instance.getAttribute('style'), 'color: red !important;');
+ var logEntries = element.takeLog();
+ assert_array_equals(logEntries.types(), ['attributeChanged']);
+ assert_attribute_log_entry(logEntries.last(), {name: 'style', oldValue: 'color: red', newValue: 'color: red !important;', namespace: null});
+ }, name + ' must enqueue an attributeChanged reaction when it makes a property important and the style attribute is observed');
+
+ test(function () {
+ var element = define_new_custom_element(['id']);
+ var instance = document.createElement(element.name);
+ instance.setAttribute('style', 'color: red');
+ assert_array_equals(element.takeLog().types(), ['constructed']);
+ testFunction(instance, 'color', 'color', true);
+ assert_equals(instance.getAttribute('style'), 'color: red !important;');
+ assert_array_equals(element.takeLog().types(), []);
+ }, name + ' must enqueue an attributeChanged reaction when it makes a property important but the style attribute is not observed');
+}
diff --git a/testing/web-platform/tests/custom-elements/reactions/with-exceptions.html b/testing/web-platform/tests/custom-elements/reactions/with-exceptions.html
new file mode 100644
index 0000000000..131348b1c4
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/reactions/with-exceptions.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Custom Elements: CEReactions interaction with exceptions</title>
+<link rel="author" title="Domenic Denicola" href="mailto:d@domenic.me">
+<meta name="help" content="https://html.spec.whatwg.org/multipage/#cereactions">
+<meta name="help" content="https://github.com/whatwg/html/pull/3235">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+
+<div id="log"></div>
+
+<script>
+"use strict";
+// Basically from https://github.com/whatwg/html/issues/3217#issuecomment-343633273
+test_with_window((contentWindow, contentDocument) => {
+ let reactionRan = false;
+ contentWindow.customElements.define("custom-element", class extends contentWindow.HTMLElement {
+ disconnectedCallback() {
+ reactionRan = true;
+ }
+ });
+ const text = contentDocument.createTextNode("");
+ contentDocument.documentElement.appendChild(text);
+ const element = contentDocument.createElement("custom-element");
+ contentDocument.documentElement.appendChild(element);
+ assert_throws_dom(
+ "HierarchyRequestError",
+ contentWindow.DOMException,
+ () => text.before("", contentDocument.documentElement)
+ );
+ assert_true(reactionRan);
+}, "Reaction must run even after the exception is thrown");
+</script>
diff --git a/testing/web-platform/tests/custom-elements/resources/custom-elements-helpers.js b/testing/web-platform/tests/custom-elements/resources/custom-elements-helpers.js
new file mode 100644
index 0000000000..48775af162
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/resources/custom-elements-helpers.js
@@ -0,0 +1,276 @@
+function create_window_in_test(t, srcdoc) {
+ let p = new Promise((resolve) => {
+ let f = document.createElement('iframe');
+ f.srcdoc = srcdoc ? srcdoc : '';
+ f.onload = (event) => {
+ let w = f.contentWindow;
+ t.add_cleanup(() => f.remove());
+ resolve(w);
+ };
+ document.body.appendChild(f);
+ });
+ return p;
+}
+
+function create_window_in_test_async(test, mime, doc) {
+ return new Promise((resolve) => {
+ let iframe = document.createElement('iframe');
+ blob = new Blob([doc], {type: mime});
+ iframe.src = URL.createObjectURL(blob);
+ iframe.onload = (event) => {
+ let contentWindow = iframe.contentWindow;
+ test.add_cleanup(() => iframe.remove());
+ resolve(contentWindow);
+ };
+ document.body.appendChild(iframe);
+ });
+}
+
+function test_with_window(f, name, srcdoc) {
+ promise_test((t) => {
+ return create_window_in_test(t, srcdoc)
+ .then((w) => {
+ f(w, w.document);
+ });
+ }, name);
+}
+
+function define_custom_element_in_window(window, name, observedAttributes) {
+ let log = [];
+
+ class CustomElement extends window.HTMLElement {
+ constructor() {
+ super();
+ log.push(create_constructor_log(this));
+ }
+ attributeChangedCallback(...args) {
+ log.push(create_attribute_changed_callback_log(this, ...args));
+ }
+ connectedCallback() { log.push(create_connected_callback_log(this)); }
+ disconnectedCallback() { log.push(create_disconnected_callback_log(this)); }
+ adoptedCallback(oldDocument, newDocument) { log.push({type: 'adopted', element: this, oldDocument: oldDocument, newDocument: newDocument}); }
+ }
+ CustomElement.observedAttributes = observedAttributes;
+
+ window.customElements.define(name, CustomElement);
+
+ return {
+ name: name,
+ class: CustomElement,
+ takeLog: function () {
+ let currentLog = log; log = [];
+ currentLog.types = () => currentLog.map((entry) => entry.type);
+ currentLog.last = () => currentLog[currentLog.length - 1];
+ return currentLog;
+ }
+ };
+}
+
+function create_constructor_log(element) {
+ return {type: 'constructed', element: element};
+}
+
+function assert_constructor_log_entry(log, element) {
+ assert_equals(log.type, 'constructed');
+ assert_equals(log.element, element);
+}
+
+function create_connected_callback_log(element) {
+ return {type: 'connected', element: element};
+}
+
+function assert_connected_log_entry(log, element) {
+ assert_equals(log.type, 'connected');
+ assert_equals(log.element, element);
+}
+
+function create_disconnected_callback_log(element) {
+ return {type: 'disconnected', element: element};
+}
+
+function assert_disconnected_log_entry(log, element) {
+ assert_equals(log.type, 'disconnected');
+ assert_equals(log.element, element);
+}
+
+function assert_adopted_log_entry(log, element) {
+ assert_equals(log.type, 'adopted');
+ assert_equals(log.element, element);
+}
+
+function create_adopted_callback_log(element) {
+ return {type: 'adopted', element: element};
+}
+
+function create_attribute_changed_callback_log(element, name, oldValue, newValue, namespace) {
+ return {
+ type: 'attributeChanged',
+ element: element,
+ name: name,
+ namespace: namespace,
+ oldValue: oldValue,
+ newValue: newValue,
+ actualValue: element.getAttributeNS(namespace, name)
+ };
+}
+
+function assert_attribute_log_entry(log, expected) {
+ assert_equals(log.type, 'attributeChanged');
+ assert_equals(log.name, expected.name);
+ assert_equals(log.oldValue, expected.oldValue);
+ assert_equals(log.newValue, expected.newValue);
+ assert_equals(log.actualValue, expected.newValue);
+ assert_equals(log.namespace, expected.namespace);
+}
+
+
+function define_new_custom_element(observedAttributes) {
+ let log = [];
+ let name = 'custom-element-' + define_new_custom_element._element_number++;
+
+ class CustomElement extends HTMLElement {
+ constructor() {
+ super();
+ log.push({type: 'constructed', element: this});
+ }
+ attributeChangedCallback(...args) {
+ log.push(create_attribute_changed_callback_log(this, ...args));
+ }
+ connectedCallback() { log.push({type: 'connected', element: this}); }
+ disconnectedCallback() { log.push({type: 'disconnected', element: this}); }
+ adoptedCallback(oldDocument, newDocument) { log.push({type: 'adopted', element: this, oldDocument: oldDocument, newDocument: newDocument}); }
+ }
+ CustomElement.observedAttributes = observedAttributes;
+
+ customElements.define(name, CustomElement);
+
+ return {
+ name: name,
+ class: CustomElement,
+ takeLog: function () {
+ let currentLog = log; log = [];
+ currentLog.types = () => currentLog.map((entry) => entry.type);
+ currentLog.last = () => currentLog[currentLog.length - 1];
+ return currentLog;
+ }
+ };
+}
+define_new_custom_element._element_number = 1;
+
+function define_build_in_custom_element(observedAttributes, extendedElement, extendsOption) {
+ let log = [];
+ let name = 'custom-element-' + define_build_in_custom_element._element_number++;
+
+ class CustomElement extends extendedElement {
+ constructor() {
+ super();
+ log.push({type: 'constructed', element: this});
+ }
+ attributeChangedCallback(...args) {
+ log.push(create_attribute_changed_callback_log(this, ...args));
+ }
+ connectedCallback() { log.push({type: 'connected', element: this}); }
+ disconnectedCallback() { log.push({type: 'disconnected', element: this}); }
+ adoptedCallback(oldDocument, newDocument) { log.push({type: 'adopted', element: this, oldDocument: oldDocument, newDocument: newDocument}); }
+ }
+ CustomElement.observedAttributes = observedAttributes;
+ customElements.define(name, CustomElement, { extends: extendsOption});
+
+ return {
+ name: name,
+ class: CustomElement,
+ takeLog: function () {
+ let currentLog = log; log = [];
+ currentLog.types = () => currentLog.map((entry) => entry.type);
+ currentLog.last = () => currentLog[currentLog.length - 1];
+ return currentLog;
+ }
+ };
+}
+define_build_in_custom_element._element_number = 1;
+
+function document_types() {
+ return [
+ {
+ name: 'the document',
+ create: function () { return Promise.resolve(document); },
+ isOwner: true,
+ hasBrowsingContext: true,
+ },
+ {
+ name: 'the document of the template elements',
+ create: function () {
+ return new Promise(function (resolve) {
+ var template = document.createElementNS('http://www.w3.org/1999/xhtml', 'template');
+ var doc = template.content.ownerDocument;
+ if (!doc.documentElement)
+ doc.appendChild(doc.createElement('html'));
+ resolve(doc);
+ });
+ },
+ hasBrowsingContext: false,
+ },
+ {
+ name: 'a new document',
+ create: function () {
+ return new Promise(function (resolve) {
+ var doc = new Document();
+ doc.appendChild(doc.createElement('html'));
+ resolve(doc);
+ });
+ },
+ hasBrowsingContext: false,
+ },
+ {
+ name: 'a cloned document',
+ create: function () {
+ return new Promise(function (resolve) {
+ var doc = document.cloneNode(false);
+ doc.appendChild(doc.createElement('html'));
+ resolve(doc);
+ });
+ },
+ hasBrowsingContext: false,
+ },
+ {
+ name: 'a document created by createHTMLDocument',
+ create: function () {
+ return Promise.resolve(document.implementation.createHTMLDocument());
+ },
+ hasBrowsingContext: false,
+ },
+ {
+ name: 'an HTML document created by createDocument',
+ create: function () {
+ return Promise.resolve(document.implementation.createDocument('http://www.w3.org/1999/xhtml', 'html', null));
+ },
+ hasBrowsingContext: false,
+ },
+ {
+ name: 'the document of an iframe',
+ create: function () {
+ return new Promise(function (resolve, reject) {
+ var iframe = document.createElement('iframe');
+ iframe.onload = function () { resolve(iframe.contentDocument); }
+ iframe.onerror = function () { reject('Failed to load an empty iframe'); }
+ document.body.appendChild(iframe);
+ });
+ },
+ hasBrowsingContext: true,
+ },
+ {
+ name: 'an HTML document fetched by XHR',
+ create: function () {
+ return new Promise(function (resolve, reject) {
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', 'resources/empty-html-document.html');
+ xhr.overrideMimeType('text/xml');
+ xhr.onload = function () { resolve(xhr.responseXML); }
+ xhr.onerror = function () { reject('Failed to fetch the document'); }
+ xhr.send();
+ });
+ },
+ hasBrowsingContext: false,
+ }
+ ];
+}
diff --git a/testing/web-platform/tests/custom-elements/resources/empty-html-document.html b/testing/web-platform/tests/custom-elements/resources/empty-html-document.html
new file mode 100644
index 0000000000..eaca3f49fd
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/resources/empty-html-document.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<html>
+<body>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/resources/my-custom-element-html-document.html b/testing/web-platform/tests/custom-elements/resources/my-custom-element-html-document.html
new file mode 100644
index 0000000000..b9bfdf90a2
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/resources/my-custom-element-html-document.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+<body>
+<my-custom-element></my-custom-element>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/resources/navigation-destination.html b/testing/web-platform/tests/custom-elements/resources/navigation-destination.html
new file mode 100644
index 0000000000..50e28f0ed8
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/resources/navigation-destination.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+ <html>
+ <body>
+ <p>Navigated!</p>
+ <script>
+ parent.postMessage('didNavigate', '*');
+ </script>
+ </body>
+ </html>
diff --git a/testing/web-platform/tests/custom-elements/scoped-registry/CustomElementRegistry-constructor.tentative.html b/testing/web-platform/tests/custom-elements/scoped-registry/CustomElementRegistry-constructor.tentative.html
new file mode 100644
index 0000000000..d80a1fbe6c
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/scoped-registry/CustomElementRegistry-constructor.tentative.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta name="author" title="Xiaocheng Hu" href="mailto:xiaochengh@chromium.org">
+<meta name="assert" content="User code can create non-global CustomElementRegistry instances and add definitions">
+<link rel="help" href="https://wicg.github.io/webcomponents/proposals/Scoped-Custom-Element-Registries">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(() => {
+ let registry = new CustomElementRegistry();
+ assert_not_equals(registry, window.customElements);
+
+ // Define an autonomous element with the new registry. It should not become a
+ // global definition.
+ class MyAutonomous extends HTMLElement {};
+ registry.define('my-autonomous', MyAutonomous);
+ assert_equals(registry.get('my-autonomous'), MyAutonomous);
+ assert_equals(window.customElements.get('my-autonomous'), undefined);
+ assert_false(document.createElement('my-autonomous') instanceof MyAutonomous);
+
+ // Do the same for a customized built-in element.
+ class MyCustomizedBuiltIn extends HTMLParagraphElement {};
+ registry.define('my-customized-builtin', MyCustomizedBuiltIn, {extends: 'p'});
+ assert_equals(registry.get('my-customized-builtin'), MyCustomizedBuiltIn);
+ assert_equals(window.customElements.get('my-customized-builtin'), undefined);
+ assert_false(document.createElement('p', {is: 'my-customized-builtin'}) instanceof MyCustomizedBuiltIn);
+}, 'Create non-global CustomElementRegistry and add definitions');
+</script>
diff --git a/testing/web-platform/tests/custom-elements/scoped-registry/CustomElementRegistry-multi-register.tentative.html b/testing/web-platform/tests/custom-elements/scoped-registry/CustomElementRegistry-multi-register.tentative.html
new file mode 100644
index 0000000000..bd97017308
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/scoped-registry/CustomElementRegistry-multi-register.tentative.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<meta name="author" title="Xiaocheng Hu" href="mailto:xiaochengh@chromium.org">
+<meta name="assert" content="The same constructor can be registered to multiple registries">
+<link rel="help" href="https://wicg.github.io/webcomponents/proposals/Scoped-Custom-Element-Registries">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+class MyCustom extends HTMLElement {};
+
+test(() => {
+ let registry1 = new CustomElementRegistry();
+ let registry2 = new CustomElementRegistry();
+ window.customElements.define('my-custom', MyCustom);
+ registry1.define('my-custom', MyCustom);
+ registry2.define('my-custom', MyCustom);
+
+ assert_equals(window.customElements.get('my-custom'), MyCustom);
+ assert_equals(registry1.get('my-custom'), MyCustom);
+ assert_equals(registry2.get('my-custom'), MyCustom);
+}, 'Same constructor can be registered to different registries');
+
+test(() => {
+ let registry = new CustomElementRegistry();
+ registry.define('custom-a', MyCustom);
+ assert_throws_dom('NotSupportedError', () => registry.define('custom-b', MyCustom));
+}, 'Non-global registries still reject duplicate registrations of the same constructor');
+</script>
diff --git a/testing/web-platform/tests/custom-elements/scoped-registry/ShadowRoot-init-registry.tentative.html b/testing/web-platform/tests/custom-elements/scoped-registry/ShadowRoot-init-registry.tentative.html
new file mode 100644
index 0000000000..f9bc5b5b56
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/scoped-registry/ShadowRoot-init-registry.tentative.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<meta name="author" title="Xiaocheng Hu" href="mailto:xiaochengh@chromium.org">
+<meta name="assert" content="User code can attach CustomElementRegistry to ShadowRoot">
+<link rel="help" href="https://wicg.github.io/webcomponents/proposals/Scoped-Custom-Element-Registries">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+function createShadowHost(testObject) {
+ let element = document.createElement('div');
+ testObject.add_cleanup(() => element.remove());
+ document.body.appendChild(element);
+ return element;
+}
+
+test(function() {
+ let host = createShadowHost(this);
+ let shadow = host.attachShadow({mode: 'open'});
+ assert_equals(shadow.registry, null);
+}, 'ShadowRoot.registry is null if not explicitly specified');
+
+test(function() {
+ let host = createShadowHost(this);
+ let shadow = host.attachShadow({mode: 'open', registry: window.customElements});
+ assert_equals(shadow.registry, window.customElements);
+}, 'Attach the global registry to a shadow root');
+
+test(function() {
+ let host = createShadowHost(this);
+ let registry = new CustomElementRegistry();
+ let shadow = host.attachShadow({mode: 'open', registry});
+ assert_equals(shadow.registry, registry);
+}, 'Attach a non-global registry to a shadow root');
+
+test(function() {
+ let registry = new CustomElementRegistry();
+ let host1 = createShadowHost(this);
+ let shadow1 = host1.attachShadow({mode: 'open', registry});
+ let host2 = createShadowHost(this);
+ let shadow2 = host2.attachShadow({mode: 'open', registry});
+ assert_equals(shadow1.registry, registry);
+ assert_equals(shadow2.registry, registry);
+}, 'Attach the same registry to multiple shadow roots');
+
+test(function() {
+ let host = createShadowHost(this);
+ let shadow = host.attachShadow({mode: 'open'});
+ shadow.registry = new CustomElementRegistry();
+ assert_equals(shadow.registry, null);
+}, 'Attaching registry to shadow root can only be done during initialization');
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/scoped-registry/ShadowRoot-innerHTML-upgrade.tentative.html b/testing/web-platform/tests/custom-elements/scoped-registry/ShadowRoot-innerHTML-upgrade.tentative.html
new file mode 100644
index 0000000000..e21c9dd033
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/scoped-registry/ShadowRoot-innerHTML-upgrade.tentative.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<meta name="author" title="Xiaocheng Hu" href="mailto:xiaochengh@chromium.org">
+<meta name="assert" content="Custom element constructors can re-enter with different definitions">
+<link rel="help" href="https://wicg.github.io/webcomponents/proposals/Scoped-Custom-Element-Registries">
+<link rel="help" href="https://github.com/WICG/webcomponents/issues/969">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="testdiv"></div>
+
+<script>
+class TestAutonomous extends HTMLElement {};
+class TestCustomizedBuiltIn extends HTMLParagraphElement {};
+
+function attachShadowForTest(t, registry) {
+ const host = document.createElement('div');
+ const shadow = host.attachShadow({mode: 'open', registry});
+ document.body.appendChild(host);
+ t.add_cleanup(() => host.remove());
+ return shadow;
+}
+
+test(t => {
+ const registry = new CustomElementRegistry;
+ registry.define('test-element', TestAutonomous);
+
+ const shadow = attachShadowForTest(t, registry);
+ shadow.innerHTML = '<test-element></test-element>';
+ assert_true(shadow.firstChild instanceof TestAutonomous, 'target tree scope');
+
+ // Verify that it doesn't pollute other tree scopes.
+ const shadow2 = attachShadowForTest(t);
+ shadow2.innerHTML = '<test-element></test-element>';
+ assert_false(shadow2.firstChild instanceof TestAutonomous, 'tree scope without registry');
+
+ const shadow3 = attachShadowForTest(t, new CustomElementRegistry);
+ shadow3.innerHTML = '<test-element></test-element>';
+ assert_false(shadow3.firstChild instanceof TestAutonomous, 'tree scope with different registry');
+
+ t.add_cleanup(() => testdiv.firstChild.remove());
+ testdiv.innerHTML = '<test-element></test-element>';
+ assert_false(testdiv.firstChild instanceof TestAutonomous, 'main document');
+}, 'Upgrade into autonomous custom element when inserted via innerHTML');
+
+test(t => {
+ const registry = new CustomElementRegistry;
+ const shadow = attachShadowForTest(t, registry);
+ shadow.innerHTML = '<test-element></test-element>';
+
+ const shadow2 = attachShadowForTest(t);
+ shadow2.innerHTML = '<test-element></test-element>';
+
+ const shadow3 = attachShadowForTest(t, new CustomElementRegistry);
+ shadow3.innerHTML = '<test-element></test-element>';
+
+ t.add_cleanup(() => testdiv.firstChild.remove());
+ testdiv.innerHTML = '<test-element></test-element>';
+
+ registry.define('test-element', TestAutonomous);
+
+ // Elements in the target tree scope should be upgraded.
+ assert_true(shadow.firstChild instanceof TestAutonomous, 'target tree scope');
+
+ // Verify that it doesn't pollute other tree scopes.
+ assert_false(shadow2.firstChild instanceof TestAutonomous, 'tree scope without registry');
+ assert_false(shadow3.firstChild instanceof TestAutonomous, 'tree scope with different registry');
+ assert_false(testdiv.firstChild instanceof TestAutonomous, 'main document');
+}, 'Upgrade into autonomous custom element when definition is added');
+
+test(t => {
+ const registry = new CustomElementRegistry;
+ registry.define('test-element', TestCustomizedBuiltIn, {extends: 'p'});
+
+ const shadow = attachShadowForTest(t, registry);
+ shadow.innerHTML = '<p is="test-element"></p>';
+ assert_true(shadow.firstChild instanceof TestCustomizedBuiltIn, 'target tree scope');
+
+ // Verify that it doesn't pollute other tree scopes.
+ const shadow2 = attachShadowForTest(t);
+ shadow2.innerHTML = '<p is="test-element"></p>';
+ assert_false(shadow2.firstChild instanceof TestCustomizedBuiltIn, 'tree scope without registry');
+
+ const shadow3 = attachShadowForTest(t, new CustomElementRegistry);
+ shadow3.innerHTML = '<p is="test-element"></p>';
+ assert_false(shadow3.firstChild instanceof TestCustomizedBuiltIn, 'tree scope with different registry');
+
+ t.add_cleanup(() => testdiv.firstChild.remove());
+ testdiv.innerHTML = '<p is="test-element"></p>';
+ assert_false(testdiv.firstChild instanceof TestCustomizedBuiltIn, 'main document');
+}, 'Upgrade into customized built-in element when inserted via innerHTML');
+
+test(t => {
+ const registry = new CustomElementRegistry;
+ const shadow = attachShadowForTest(t, registry);
+ shadow.innerHTML = '<p is="test-element"></p>';
+
+ const shadow2 = attachShadowForTest(t);
+ shadow2.innerHTML = '<p is="test-element"></p>';
+
+ const shadow3 = attachShadowForTest(t, new CustomElementRegistry);
+ shadow3.innerHTML = '<p is="test-element"></p>';
+
+ t.add_cleanup(() => testdiv.firstChild.remove());
+ testdiv.innerHTML = '<p is="test-element"></p>';
+
+ registry.define('test-element', TestCustomizedBuiltIn, {extends: 'p'});
+
+ // Elements in the target tree scope should be upgraded.
+ assert_true(shadow.firstChild instanceof TestCustomizedBuiltIn, 'target tree scope');
+
+ // Verify that it doesn't pollute other tree scopes.
+ assert_false(shadow2.firstChild instanceof TestCustomizedBuiltIn, 'tree scope without registry');
+ assert_false(shadow3.firstChild instanceof TestCustomizedBuiltIn, 'tree scope with different registry');
+ assert_false(testdiv.firstChild instanceof TestCustomizedBuiltIn, 'main document');
+}, 'Upgrade into customized built-in element when definition is added');
+</script>
diff --git a/testing/web-platform/tests/custom-elements/scoped-registry/constructor-call.tentative.html b/testing/web-platform/tests/custom-elements/scoped-registry/constructor-call.tentative.html
new file mode 100644
index 0000000000..19a8e3f4d8
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/scoped-registry/constructor-call.tentative.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<meta name="author" title="Xiaocheng Hu" href="mailto:xiaochengh@chromium.org">
+<meta name="assert" content="Direct calls of custom element constructor use the global registry only">
+<link rel="help" href="https://wicg.github.io/webcomponents/proposals/Scoped-Custom-Element-Registries">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body>
+<script>
+function attachShadowForTest(t, registry) {
+ const host = document.createElement('div');
+ const shadow = host.attachShadow({mode: 'open', registry});
+ document.body.appendChild(host);
+ t.add_cleanup(() => host.remove());
+ return shadow;
+}
+
+test(t => {
+ class TestElement extends HTMLElement {};
+ let registry = new CustomElementRegistry()
+ registry.define('test-element', TestElement);
+
+ let shadow = attachShadowForTest(t, registry);
+
+ assert_throws_js(TypeError, () => new TestElement);
+}, 'Calling custom element constructor directly without global registration should fail');
+
+test(t => {
+ class TestElement extends HTMLElement {};
+
+ window.customElements.define('global-test-element', TestElement);
+
+ let registry = new CustomElementRegistry()
+ registry.define('shadow-test-element', TestElement);
+ let shadow = attachShadowForTest(t, registry);
+
+ let element = new TestElement;
+ assert_equals(element.localName, 'global-test-element');
+}, 'Calling custom element constructor directly uses global registration only');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/scoped-registry/constructor-reentry-with-different-definition.tentative.html b/testing/web-platform/tests/custom-elements/scoped-registry/constructor-reentry-with-different-definition.tentative.html
new file mode 100644
index 0000000000..dc93e3c702
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/scoped-registry/constructor-reentry-with-different-definition.tentative.html
@@ -0,0 +1,137 @@
+<!DOCTYPE html>
+<meta name="author" title="Xiaocheng Hu" href="mailto:xiaochengh@chromium.org">
+<meta name="assert" content="Custom element constructors can re-enter with different definitions">
+<link rel="help" href="https://wicg.github.io/webcomponents/proposals/Scoped-Custom-Element-Registries">
+<link rel="help" href="https://github.com/WICG/webcomponents/issues/969">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id='test-container-1'></div>
+<div id='test-container-2'></div>
+
+<script>
+setup({allow_uncaught_exception : true});
+
+function createShadowForTest(t, registry) {
+ const host = document.createElement('div');
+ const shadow = host.attachShadow({mode: 'open', registry});
+ document.body.appendChild(host);
+ t.add_cleanup(() => host.remove());
+ return shadow;
+}
+
+test(t => {
+ let needsTest = true;
+ class ReentryBeforeSuper extends HTMLElement {
+ constructor() {
+ if (needsTest) {
+ needsTest = false;
+ document.getElementById('test-container-1').innerHTML =
+ '<test-element-1></test-element-1>';
+ }
+ super();
+ }
+ };
+ window.customElements.define('test-element-1', ReentryBeforeSuper);
+
+ let registry = new CustomElementRegistry;
+ registry.define('shadow-test-element-1', ReentryBeforeSuper);
+
+ let shadow = createShadowForTest(t, registry);
+ shadow.innerHTML = '<shadow-test-element-1></shadow-test-element-1>';
+
+ let shadowElement = shadow.firstChild;
+ assert_true(shadowElement instanceof ReentryBeforeSuper);
+ assert_equals(shadowElement.localName, 'shadow-test-element-1');
+
+ let mainDocElement = document.getElementById('test-container-1').firstChild;
+ assert_true(mainDocElement instanceof ReentryBeforeSuper);
+ assert_equals(mainDocElement.localName, 'test-element-1');
+}, 'Re-entry via upgrade before calling super()');
+
+test(t => {
+ let needsTest = true;
+ class ReentryAfterSuper extends HTMLElement {
+ constructor() {
+ super();
+ if (needsTest) {
+ needsTest = false;
+ document.getElementById('test-container-2').innerHTML =
+ '<test-element-2></test-element-2>';
+ }
+ }
+ };
+ window.customElements.define('test-element-2', ReentryAfterSuper);
+
+ let registry = new CustomElementRegistry;
+ registry.define('shadow-test-element-2', ReentryAfterSuper);
+
+ let shadow = createShadowForTest(t, registry);
+ shadow.innerHTML = '<shadow-test-element-2></shadow-test-element-2>';
+
+ let shadowElement = shadow.firstChild;
+ assert_true(shadowElement instanceof ReentryAfterSuper);
+ assert_equals(shadowElement.localName, 'shadow-test-element-2');
+
+ let mainDocElement = document.getElementById('test-container-2').firstChild;
+ assert_true(mainDocElement instanceof ReentryAfterSuper);
+ assert_equals(mainDocElement.localName, 'test-element-2');
+}, 'Re-entry via upgrade after calling super()');
+
+test(t => {
+ let needsTest = true;
+ let elementByNestedCall;
+ class ReentryByDirectCall extends HTMLElement {
+ constructor() {
+ if (needsTest) {
+ needsTest = false;
+ elementByNestedCall = new ReentryByDirectCall;
+ }
+ super();
+ }
+ }
+ window.customElements.define('test-element-3', ReentryByDirectCall);
+
+ let registry = new CustomElementRegistry;
+ registry.define('shadow-test-element-3', ReentryByDirectCall);
+
+ let shadow = createShadowForTest(t, registry);
+ shadow.innerHTML = '<shadow-test-element-3></shadow-test-element-3>';
+
+ let shadowElement = shadow.firstChild;
+ assert_true(shadowElement instanceof ReentryByDirectCall);
+ assert_equals(shadowElement.localName, 'shadow-test-element-3');
+
+ // Nested constructor call makes the following `super()` fail, and we should
+ // end up creating only one element.
+ assert_equals(elementByNestedCall, shadowElement);
+}, 'Re-entry via direct constructor call before calling super()');
+
+test(t => {
+ let needsTest = true;
+ let elementByNestedCall;
+ class ReentryByDirectCall extends HTMLElement {
+ constructor() {
+ super();
+ if (needsTest) {
+ needsTest = false;
+ elementByNestedCall = new ReentryByDirectCall;
+ }
+ }
+ }
+ window.customElements.define('test-element-4', ReentryByDirectCall);
+
+ let registry = new CustomElementRegistry;
+ registry.define('shadow-test-element-4', ReentryByDirectCall);
+
+ let shadow = createShadowForTest(t, registry);
+ shadow.innerHTML = '<shadow-test-element-4></shadow-test-element-4>';
+
+ let shadowElement = shadow.firstChild;
+ assert_true(shadowElement instanceof ReentryByDirectCall);
+ assert_equals(shadowElement.localName, 'shadow-test-element-4');
+
+ // Nested constructor call should be blocked.
+ assert_false(elementByNestedCall instanceof ReentryByDirectCall);
+}, 'Re-entry via direct constructor call after calling super()');
+</script>
diff --git a/testing/web-platform/tests/custom-elements/state/tentative/ElementInternals-states.html b/testing/web-platform/tests/custom-elements/state/tentative/ElementInternals-states.html
new file mode 100644
index 0000000000..96dcb841ee
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/state/tentative/ElementInternals-states.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+class TestElement extends HTMLElement {
+ constructor() {
+ super();
+ this._internals = this.attachInternals();
+ }
+
+ get internals() {
+ return this._internals;
+ }
+}
+customElements.define("test-element", TestElement);
+
+test(() => {
+ let i = (new TestElement()).internals;
+
+ assert_true(i.states instanceof CustomStateSet);
+ assert_equals(i.states.size, 0);
+ assert_false(i.states.has('foo'));
+ assert_false(i.states.has('--foo'));
+ assert_equals(i.states.toString(), '[object CustomStateSet]');
+}, 'CustomStateSet behavior of ElementInternals.states: Initial state');
+
+test(() => {
+ let i = (new TestElement()).internals;
+ assert_throws_js(TypeError, () => { i.states.supports('foo'); });
+ assert_throws_dom('SyntaxError', () => { i.states.add(''); });
+ assert_throws_dom('SyntaxError', () => { i.states.add('--a\tb'); });
+}, 'CustomStateSet behavior of ElementInternals.states: Exceptions');
+
+test(() => {
+ let i = (new TestElement()).internals;
+ i.states.add('--foo');
+ i.states.add('--bar');
+ i.states.add('--foo');
+ assert_equals(i.states.size, 2);
+ assert_true(i.states.has('--foo'));
+ assert_true(i.states.has('--bar'));
+ assert_array_equals([...i.states], ['--foo', '--bar']);
+ i.states.delete('--foo');
+ assert_array_equals([...i.states], ['--bar']);
+ i.states.add('--foo');
+ assert_array_equals([...i.states], ['--bar', '--foo']);
+ i.states.delete('--bar');
+ i.states.add('--baz');
+ assert_array_equals([...i.states], ['--foo', '--baz']);
+}, 'CustomStateSet behavior of ElementInternals.states: Modifications');
+
+test(() => {
+ let i = (new TestElement()).internals;
+ i.states.add('--one');
+ i.states.add('--two');
+ i.states.add('--three');
+ let iter = i.states.values();
+
+ // Delete the next item.
+ i.states.delete('--one');
+ let item = iter.next();
+ assert_false(item.done);
+ assert_equals(item.value, '--two');
+
+ // Clear the set.
+ i.states.clear();
+ item = iter.next();
+ assert_true(item.done);
+
+ // Delete the previous item.
+ i.states.add('--one');
+ i.states.add('--two');
+ i.states.add('--three');
+ iter = i.states.values();
+ item = iter.next();
+ assert_equals(item.value, '--one');
+ i.states.delete('--one');
+ item = iter.next();
+ assert_equals(item.value, '--two');
+}, 'Updating a CustomStateSet while iterating it should work');
+</script>
+
diff --git a/testing/web-platform/tests/custom-elements/state/tentative/README.md b/testing/web-platform/tests/custom-elements/state/tentative/README.md
new file mode 100644
index 0000000000..b11784bd07
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/state/tentative/README.md
@@ -0,0 +1 @@
+Tests for [Custom State Pseudo Class](https://wicg.github.io/custom-state-pseudo-class/)
diff --git a/testing/web-platform/tests/custom-elements/state/tentative/state-pseudo-class.html b/testing/web-platform/tests/custom-elements/state/tentative/state-pseudo-class.html
new file mode 100644
index 0000000000..3e3806a042
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/state/tentative/state-pseudo-class.html
@@ -0,0 +1,132 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+#state-and-part::part(inner) {
+ opacity: 0;
+}
+#state-and-part::part(inner):--innerFoo {
+ opacity: 0.5;
+}
+#state-and-part:--outerFoo::part(inner) {
+ opacity: 0.25;
+}
+:--\(escaped\ state {}
+</style>
+<body>
+<script>
+class TestElement extends HTMLElement {
+ constructor() {
+ super();
+ this._internals = this.attachInternals();
+ }
+
+ get i() {
+ return this._internals;
+ }
+}
+customElements.define('test-element', TestElement);
+
+class ContainerElement extends HTMLElement {
+ constructor() {
+ super();
+ this._internals = this.attachInternals();
+ this._shadow = this.attachShadow({mode:'open'});
+ this._shadow.innerHTML = `
+<style>
+:host {
+ border-style: solid;
+}
+:host(:--dotted) {
+ border-style: dotted;
+}
+</style>
+<test-element part="inner"></test-element>`;
+ }
+
+ get i() {
+ return this._internals;
+ }
+ get innerElement() {
+ return this._shadow.querySelector('test-element');
+ }
+}
+customElements.define('container-element', ContainerElement);
+
+test(() => {
+ document.querySelector(':--');
+ document.querySelector(':--16px');
+}, ':--foo parsing passes');
+
+test(() => {
+ assert_throws_dom('SyntaxError', () => { document.querySelector(':--('); });
+ assert_throws_dom('SyntaxError', () => { document.querySelector(':--)'); });
+ assert_throws_dom('SyntaxError', () => { document.querySelector(':--='); });
+ assert_throws_dom('SyntaxError', () => { document.querySelector(':--name=value'); });
+}, ':--foo parsing failures');
+
+test(() => {
+ assert_equals(document.styleSheets[0].cssRules[1].cssText,
+ '#state-and-part::part(inner):--innerFoo { opacity: 0.5; }');
+ assert_equals(document.styleSheets[0].cssRules[3].selectorText,
+ ':--\\(escaped\\ state');
+}, ':--foo serialization');
+
+test(() => {
+ let element = new TestElement();
+ let states = element.i.states;
+
+ assert_false(element.matches(':--foo'));
+ assert_true(element.matches(':not(:--foo)'));
+ states.add('--foo');
+ assert_true(element.matches(':--foo'));
+ assert_true(element.matches(':is(:--foo)'));
+ element.classList.add('c1', 'c2');
+ assert_true(element.matches('.c1:--foo'));
+ assert_true(element.matches(':--foo.c1'));
+ assert_true(element.matches('.c2:--foo.c1'));
+}, ':--foo in simple cases');
+
+test(() => {
+ let element = new TestElement();
+ element.tabIndex = 0;
+ document.body.appendChild(element);
+ element.focus();
+ let states = element.i.states;
+
+ states.add('--foo');
+ assert_true(element.matches(':focus:--foo'));
+ assert_true(element.matches(':--foo:focus'));
+}, ':--foo and other pseudo classes');
+
+test(() => {
+ let outer = new ContainerElement();
+ outer.id = 'state-and-part';
+ document.body.appendChild(outer);
+ let inner = outer.innerElement;
+ let innerStates = inner.i.states;
+
+ innerStates.add('--innerFoo');
+ assert_equals(getComputedStyle(inner).opacity, '0.5',
+ '::part() followed by :--foo');
+ innerStates.delete('--innerFoo');
+ innerStates.add('--innerfoo');
+ assert_equals(getComputedStyle(inner).opacity, '0',
+ ':--foo matching should be case-sensitive');
+ innerStates.delete('--innerfoo');
+
+ outer.i.states.add('--outerFoo');
+ assert_equals(getComputedStyle(inner).opacity, '0.25',
+ ':--foo followed by ::part()');
+}, ':--foo and ::part()');
+
+test(() => {
+ let outer = new ContainerElement();
+ document.body.appendChild(outer);
+
+ assert_equals(getComputedStyle(outer).borderStyle, 'solid');
+ outer.i.states.add('--dotted');
+ assert_equals(getComputedStyle(outer).borderStyle, 'dotted');
+}, ':--foo and :host()');
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/throw-on-dynamic-markup-insertion-counter-construct-xml-parser.xhtml b/testing/web-platform/tests/custom-elements/throw-on-dynamic-markup-insertion-counter-construct-xml-parser.xhtml
new file mode 100644
index 0000000000..c0a7f622fb
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/throw-on-dynamic-markup-insertion-counter-construct-xml-parser.xhtml
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="utf-8"?>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<title>Custom Elements: create an element for a token must increment and decrement document's throw-on-dynamic-markup-insertion counter</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org" />
+<meta name="assert" content="Invoking document.open, document.write, document.writeln, and document.write must throw an exception when the HTML parser is creating a custom element for a token" />
+<meta name="help" content="https://html.spec.whatwg.org/multipage/parsing.html#create-an-element-for-the-token" />
+<meta name="help" content="https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#throw-on-dynamic-markup-insertion-counter" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/custom-elements-helpers.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+<![CDATA[
+
+async function construct_custom_element_in_parser(test, code)
+{
+ window.executed = false;
+ window.exception = false;
+ const content_window = await create_window_in_test_async(test, 'application/xml', `<?xml version="1.0" encoding="utf-8"?>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<script>
+<![CDATA[
+let executed = false;
+let exception = null;
+class CustomElement extends window.HTMLElement {
+ constructor() {
+ super();
+ try {
+ ${code}
+ } catch (error) {
+ exception = error;
+ }
+ executed = true;
+ }
+}
+customElements.define('some-element', CustomElement);
+]]` + `>
+</` + `script>
+</head>
+<body>
+<some-element></some-element>
+<script>
+top.executed = executed;
+top.exception = exception;
+</script>
+</body>
+</html>`);
+ let content_document;
+ try {
+ content_document = content_window.document;
+ } catch (error) { }
+ assert_true(executed, 'Must synchronously instantiate a custom element');
+ return {window: content_window, document: content_document, exception};
+}
+
+promise_test(async function () {
+ const result = await construct_custom_element_in_parser(this, `document.open()`);
+ assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError');
+ }, 'document.open() must throw an InvalidStateError when synchronously constructing a custom element');
+
+promise_test(async function () {
+ const result = await construct_custom_element_in_parser(this, `document.open('text/html')`);
+ assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError');
+}, 'document.open("text/html") must throw an InvalidStateError when synchronously constructing a custom element');
+
+// https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-document-open-window
+promise_test(async function () {
+ let load_promise = new Promise((resolve) => window.onmessage = (event) => resolve(event.data));
+ const url = top.location.href.substring(0, top.location.href.lastIndexOf('/')) + '/resources/navigation-destination.html';
+ const result = await construct_custom_element_in_parser(this, `document.open('${url}', '_self', '')`);
+ assert_equals(result.exception, null);
+ assert_equals(await load_promise, 'didNavigate');
+}, 'document.open(URL) must NOT throw an InvalidStateError when synchronously constructing a custom element');
+
+promise_test(async function () {
+ const result = await construct_custom_element_in_parser(this, `document.close()`);
+ assert_not_equals(result.exception, null);
+ assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError');
+}, 'document.close() must throw an InvalidStateError when synchronously constructing a custom element');
+
+promise_test(async function () {
+ const result = await construct_custom_element_in_parser(this, `document.write('<b>some text</b>')`);
+ assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError');
+ assert_equals(result.document.querySelector('b'), null, 'Must not insert new content');
+ assert_false(result.document.body.innerHTML.includes('some text'), 'Must not insert new content');
+}, 'document.write must throw an InvalidStateError when synchronously constructing a custom element');
+
+promise_test(async function () {
+ const result = await construct_custom_element_in_parser(this, `document.writeln('<b>some text</b>')`);
+ assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError');
+ assert_equals(result.document.querySelector('b'), null, 'Must not insert new content');
+ assert_false(result.document.body.innerHTML.includes('some text'), 'Must not insert new content');
+}, 'document.writeln must throw an InvalidStateError when synchronously constructing a custom element');
+
+promise_test(async function () {
+ window.another_window = await create_window_in_test_async(this, 'text/html', '<!DOCTYPE html><html><body>');
+ const result = await construct_custom_element_in_parser(this, `top.another_window.document.open()`);
+ assert_equals(result.exception, null);
+}, 'document.open() of another document must not throw an InvalidStateError when synchronously constructing a custom element');
+
+promise_test(async function () {
+ window.another_window = await create_window_in_test_async(this, 'text/html', '<!DOCTYPE html><html><body>');
+ const result = await construct_custom_element_in_parser(this, `top.another_window.document.open('text/html')`);
+ assert_equals(result.exception, null);
+}, 'document.open("text/html") of another document must not throw an InvalidStateError when synchronously constructing a custom element');
+
+promise_test(async function () {
+ window.another_window = await create_window_in_test_async(this, 'text/html', '<!DOCTYPE html><html><body>');
+ const result = await construct_custom_element_in_parser(this, `top.another_window.document.close()`);
+ assert_equals(result.exception, null);
+}, 'document.close() of another document must not throw an InvalidStateError when synchronously constructing a custom element');
+
+promise_test(async function () {
+ window.another_window = await create_window_in_test_async(this, 'text/html', '<!DOCTYPE html><html><body>');
+ const result = await construct_custom_element_in_parser(this, `top.another_window.document.write('<b>some text</b>')`);
+ assert_equals(result.exception, null);
+ assert_equals(another_window.document.querySelector('b').outerHTML, '<b>some text</b>');
+}, 'document.write of another document must not throw an InvalidStateError when synchronously constructing a custom element');
+
+promise_test(async function () {
+ window.another_window = await create_window_in_test_async(this, 'text/html', '<!DOCTYPE html><html><body>');
+ const result = await construct_custom_element_in_parser(this, `top.another_window.document.writeln('<b>some text</b>')`);
+ assert_equals(result.exception, null);
+ assert_equals(another_window.document.querySelector('b').outerHTML, '<b>some text</b>');
+}, 'document.writeln of another document must not throw an InvalidStateError when synchronously constructing a custom element');
+
+]]>
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/throw-on-dynamic-markup-insertion-counter-construct.html b/testing/web-platform/tests/custom-elements/throw-on-dynamic-markup-insertion-counter-construct.html
new file mode 100644
index 0000000000..3e9254e2a5
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/throw-on-dynamic-markup-insertion-counter-construct.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+ <html>
+ <head>
+ <title>Custom Elements: create an element for a token must increment and decrement document's throw-on-dynamic-markup-insertion counter</title>
+ <meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+ <meta name="assert" content="Invoking document.open, document.write, document.writeln, and document.write must throw an exception when the HTML parser is creating a custom element for a token">
+ <meta name="help" content="https://html.spec.whatwg.org/multipage/parsing.html#create-an-element-for-the-token">
+ <meta name="help" content="https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#throw-on-dynamic-markup-insertion-counter">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="./resources/custom-elements-helpers.js"></script>
+ </head>
+ <body>
+ <div id="log"></div>
+ <script>
+
+ async function construct_custom_element_in_parser(test, call_function)
+ {
+ const window = await create_window_in_test(test);
+ const document = window.document;
+
+ document.open();
+
+ let executed = false;
+ let exception = null;
+ class CustomElement extends window.HTMLElement {
+ constructor() {
+ super();
+ try {
+ call_function(document, window);
+ } catch (error) {
+ exception = error;
+ }
+ executed = true;
+ }
+ }
+ window.customElements.define('some-element', CustomElement);
+
+ document.write('<!DOCTYPE html><html><body><some-element></some-element></body></html>');
+ document.close();
+
+ assert_true(executed, 'Must synchronously instantiate a custom element');
+ return {window, document, exception};
+ }
+
+ promise_test(async function () {
+ const result = await construct_custom_element_in_parser(this, (document) => document.open());
+ assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError');
+ }, 'document.open() must throw an InvalidStateError when synchronously constructing a custom element');
+
+ promise_test(async function () {
+ const result = await construct_custom_element_in_parser(this, (document) => document.open('text/html'));
+ assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError');
+ }, 'document.open("text/html") must throw an InvalidStateError when synchronously constructing a custom element');
+
+ // https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-document-open-window
+ promise_test(async function () {
+ let load_promise = new Promise((resolve) => window.onmessage = (event) => resolve(event.data));
+ const result = await construct_custom_element_in_parser(this, (document, window) => document.open('resources/navigation-destination.html', '_self', ''));
+ assert_equals(result.exception, null);
+ assert_equals(await load_promise, 'didNavigate');
+ }, 'document.open(URL) must NOT throw an InvalidStateError when synchronously constructing a custom element');
+
+ promise_test(async function () {
+ const result = await construct_custom_element_in_parser(this, (document) => document.close());
+ assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError');
+ }, 'document.close() must throw an InvalidStateError when synchronously constructing a custom element');
+
+ promise_test(async function () {
+ const result = await construct_custom_element_in_parser(this, (document) => document.write('<b>some text</b>'));
+ assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError');
+ assert_equals(result.document.querySelector('b'), null, 'Must not insert new content');
+ assert_false(result.document.documentElement.innerHTML.includes('some text'), 'Must not insert new content');
+ }, 'document.write must throw an InvalidStateError when synchronously constructing a custom element');
+
+ promise_test(async function () {
+ const result = await construct_custom_element_in_parser(this, (document) => document.writeln('<b>some text</b>'));
+ assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError');
+ assert_equals(result.document.querySelector('b'), null, 'Must not insert new content');
+ assert_false(result.document.documentElement.innerHTML.includes('some text'), 'Must not insert new content');
+ }, 'document.writeln must throw an InvalidStateError when synchronously constructing a custom element');
+
+ promise_test(async function () {
+ const another_window = await create_window_in_test(this);
+ const result = await construct_custom_element_in_parser(this, (document) => another_window.document.open());
+ assert_equals(result.exception, null);
+ }, 'document.open() of another document must not throw an InvalidStateError when synchronously constructing a custom element');
+
+ promise_test(async function () {
+ const another_window = await create_window_in_test(this);
+ const result = await construct_custom_element_in_parser(this, (document) => another_window.document.open('text/html'));
+ assert_equals(result.exception, null);
+ }, 'document.open("text/html") of another document must not throw an InvalidStateError when synchronously constructing a custom element');
+
+ promise_test(async function () {
+ const another_window = await create_window_in_test(this);
+ const result = await construct_custom_element_in_parser(this, (document) => another_window.document.close());
+ assert_equals(result.exception, null);
+ }, 'document.close() of another document must not throw an InvalidStateError when synchronously constructing a custom element');
+
+ promise_test(async function () {
+ const another_window = await create_window_in_test(this);
+ const result = await construct_custom_element_in_parser(this, (document) => another_window.document.write('<b>some text</b>'));
+ assert_equals(result.exception, null);
+ assert_equals(another_window.document.querySelector('b').outerHTML, '<b>some text</b>');
+ }, 'document.write of another document must not throw an InvalidStateError when synchronously constructing a custom element');
+
+ promise_test(async function () {
+ const another_window = await create_window_in_test(this);
+ const result = await construct_custom_element_in_parser(this, (document) => another_window.document.writeln('<b>some text</b>'));
+ assert_equals(result.exception, null);
+ assert_equals(another_window.document.querySelector('b').outerHTML, '<b>some text</b>');
+ }, 'document.writeln of another document must not throw an InvalidStateError when synchronously constructing a custom element');
+
+ </script>
+ </body>
+ </html>
diff --git a/testing/web-platform/tests/custom-elements/throw-on-dynamic-markup-insertion-counter-reactions-xml-parser.xhtml b/testing/web-platform/tests/custom-elements/throw-on-dynamic-markup-insertion-counter-reactions-xml-parser.xhtml
new file mode 100644
index 0000000000..13f664550b
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/throw-on-dynamic-markup-insertion-counter-reactions-xml-parser.xhtml
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="utf-8"?>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<title>Custom Elements: create an element for a token must increment and decrement document's throw-on-dynamic-markup-insertion counter</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org" />
+<meta name="assert" content="Invoking document.open, document.write, document.writeln, and document.write must throw an exception when the HTML parser is creating a custom element for a token" />
+<meta name="help" content="https://html.spec.whatwg.org/multipage/parsing.html#create-an-element-for-the-token" />
+<meta name="help" content="https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#throw-on-dynamic-markup-insertion-counter" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/custom-elements-helpers.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+<![CDATA[
+
+async function custom_element_reactions_in_parser(test, code)
+{
+ window.executed = false;
+ window.exception = false;
+ const content_window = await create_window_in_test_async(test, 'application/xml', `<?xml version="1.0" encoding="utf-8"?>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<script>
+<![CDATA[
+let executed = false;
+let exception = null;
+class CustomElement extends window.HTMLElement {
+ constructor() {
+ super();
+ try {
+ ${code}
+ } catch (error) {
+ exception = error;
+ }
+ executed = true;
+ }
+}
+CustomElement.observedAttributes = ['title'];
+customElements.define('some-element', CustomElement);
+]]` + `>
+</` + `script>
+</head>
+<body>
+<some-element title="some title"></some-element>
+<script>
+top.executed = executed;
+top.exception = exception;
+</script>
+</body>
+</html>`);
+ let content_document;
+ try {
+ content_document = content_window.document;
+ } catch (error) { }
+ assert_true(executed, 'Must immediately process custom element reactions for setting attributes');
+ return {window: content_window, document: content_document, exception};
+}
+
+promise_test(async function () {
+ const result = await custom_element_reactions_in_parser(this, `document.open()`);
+ assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError');
+}, 'document.open() must throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element');
+
+promise_test(async function () {
+ const result = await custom_element_reactions_in_parser(this, `document.open('text/html')`);
+ assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError');
+}, 'document.open("text/html") must throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element');
+
+// https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-document-open-window
+promise_test(async function () {
+ let load_promise = new Promise((resolve) => window.onmessage = (event) => resolve(event.data));
+ const url = top.location.href.substring(0, top.location.href.lastIndexOf('/')) + '/resources/navigation-destination.html';
+ const result = await custom_element_reactions_in_parser(this, `document.open('${url}', '_self', '')`);
+ assert_equals(result.exception, null);
+ assert_equals(await load_promise, 'didNavigate');
+}, 'document.open(URL) must NOT throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element');
+
+promise_test(async function () {
+ const result = await custom_element_reactions_in_parser(this, `document.close()`);
+ assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError');
+}, 'document.close() must throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element');
+
+promise_test(async function () {
+ const result = await custom_element_reactions_in_parser(this, `document.write('<b>some text</b>')`);
+ assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError');
+ assert_equals(result.document.querySelector('b'), null, 'Must not insert new content');
+ assert_false(result.document.body.innerHTML.includes('some text'), 'Must not insert new content');
+}, 'document.write must throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element');
+
+promise_test(async function () {
+ const result = await custom_element_reactions_in_parser(this, `document.writeln('<b>some text</b>')`);
+ assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError');
+ assert_equals(result.document.querySelector('b'), null, 'Must not insert new content');
+ assert_false(result.document.body.innerHTML.includes('some text'), 'Must not insert new content');
+}, 'document.writeln must throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element');
+
+promise_test(async function () {
+ window.another_window = await create_window_in_test(this);
+ const result = await custom_element_reactions_in_parser(this, `top.another_window.document.open()`);
+ assert_equals(result.exception, null);
+}, 'document.open() of another document must not throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element');
+
+promise_test(async function () {
+ window.another_window = await create_window_in_test(this);
+ const result = await custom_element_reactions_in_parser(this, `top.another_window.document.open('text/html')`);
+ assert_equals(result.exception, null);
+}, 'document.open("text/html") of another document must not throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element');
+
+promise_test(async function () {
+ window.another_window = await create_window_in_test(this);
+ const result = await custom_element_reactions_in_parser(this, `top.another_window.document.close()`);
+ assert_equals(result.exception, null);
+}, 'document.close() of another document must not throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element');
+
+promise_test(async function () {
+ window.another_window = await create_window_in_test(this);
+ const result = await custom_element_reactions_in_parser(this, `top.another_window.document.write('<b>some text</b>')`);
+ assert_equals(result.exception, null);
+ assert_equals(another_window.document.querySelector('b').outerHTML, '<b>some text</b>');
+}, 'document.write of another document must not throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element');
+
+promise_test(async function () {
+ window.another_window = await create_window_in_test(this);
+ const result = await custom_element_reactions_in_parser(this, `top.another_window.document.writeln('<b>some text</b>')`);
+ assert_equals(result.exception, null);
+ assert_equals(another_window.document.querySelector('b').outerHTML, '<b>some text</b>');
+}, 'document.writeln of another document must not throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element');
+
+]]>
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/throw-on-dynamic-markup-insertion-counter-reactions.html b/testing/web-platform/tests/custom-elements/throw-on-dynamic-markup-insertion-counter-reactions.html
new file mode 100644
index 0000000000..e798d332e3
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/throw-on-dynamic-markup-insertion-counter-reactions.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: create an element for a token must increment and decrement document's throw-on-dynamic-markup-insertion counter</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="Invoking document.open, document.write, document.writeln, and document.write must throw an exception when the HTML parser is creating a custom element for a token">
+ <meta name="help" content="https://html.spec.whatwg.org/multipage/parsing.html#create-an-element-for-the-token">
+<meta name="help" content="https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#throw-on-dynamic-markup-insertion-counter">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/custom-elements-helpers.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+
+async function custom_element_reactions_in_parser(test, call_function)
+{
+ const window = await create_window_in_test(test);
+ const document = window.document;
+
+ document.open();
+
+ let executed = false;
+ let exception = null;
+ class CustomElement extends window.HTMLElement {
+ attributeChangedCallback(name, oldValue, newValue) {
+ try {
+ call_function(document, window);
+ } catch (error) {
+ exception = error;
+ }
+ executed = true;
+ }
+ }
+ CustomElement.observedAttributes = ['title'];
+ window.customElements.define('some-element', CustomElement);
+
+ document.write('<!DOCTYPE html><html><body><some-element title="some title"></some-element></body></html>');
+ document.close();
+
+ assert_true(executed, 'Must immediately process custom element reactions for setting attributes');
+ return {frameElement, window, document, exception};
+}
+
+promise_test(async function () {
+ const result = await custom_element_reactions_in_parser(this, (document) => document.open());
+ assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError');
+}, 'document.open() must throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element');
+
+promise_test(async function () {
+ const result = await custom_element_reactions_in_parser(this, (document) => document.open('text/html'));
+ assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError');
+}, 'document.open("text/html") must throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element');
+
+// https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-document-open-window
+promise_test(async function () {
+ let load_promise = new Promise((resolve) => window.onmessage = (event) => resolve(event.data));
+ const result = await custom_element_reactions_in_parser(this, (document, window) => document.open('resources/navigation-destination.html', '_self', ''));
+ assert_equals(result.exception, null);
+ assert_equals(await load_promise, 'didNavigate');
+}, 'document.open(URL) must NOT throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element');
+
+promise_test(async function () {
+ const result = await custom_element_reactions_in_parser(this, (document) => document.close());
+ assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError');
+}, 'document.close() must throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element');
+
+promise_test(async function () {
+ const result = await custom_element_reactions_in_parser(this, (document) => document.write('<b>some text</b>'));
+ assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError');
+ assert_equals(result.document.querySelector('b'), null, 'Must not insert new content');
+ assert_false(result.document.documentElement.innerHTML.includes('some text'), 'Must not insert new content');
+}, 'document.write must throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element');
+
+promise_test(async function () {
+ const result = await custom_element_reactions_in_parser(this, (document) => document.writeln('<b>some text</b>'));
+ assert_throws_dom('InvalidStateError', result.window.DOMException, () => { throw result.exception; }, 'Must throw an InvalidStateError');
+ assert_equals(result.document.querySelector('b'), null, 'Must not insert new content');
+ assert_false(result.document.documentElement.innerHTML.includes('some text'), 'Must not insert new content');
+}, 'document.writeln must throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element');
+
+promise_test(async function () {
+ const another_window = await create_window_in_test(this);
+ const result = await custom_element_reactions_in_parser(this, (document) => another_window.document.open());
+ assert_equals(result.exception, null);
+}, 'document.open() of another document must not throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element');
+
+promise_test(async function () {
+ const another_window = await create_window_in_test(this);
+ const result = await custom_element_reactions_in_parser(this, (document) => another_window.document.open('text/html'));
+ assert_equals(result.exception, null);
+}, 'document.open("text/html") of another document must not throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element');
+
+promise_test(async function () {
+ const another_window = await create_window_in_test(this);
+ const result = await custom_element_reactions_in_parser(this, (document) => another_window.document.close());
+ assert_equals(result.exception, null);
+}, 'document.close() of another document must not throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element');
+
+promise_test(async function () {
+ const another_window = await create_window_in_test(this);
+ const result = await custom_element_reactions_in_parser(this, (document) => another_window.document.write('<b>some text</b>'));
+ assert_equals(result.exception, null);
+ assert_equals(another_window.document.querySelector('b').outerHTML, '<b>some text</b>');
+}, 'document.write of another document must not throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element');
+
+promise_test(async function () {
+ const another_window = await create_window_in_test(this);
+ const result = await custom_element_reactions_in_parser(this, (document) => another_window.document.writeln('<b>some text</b>'));
+ assert_equals(result.exception, null);
+ assert_equals(another_window.document.querySelector('b').outerHTML, '<b>some text</b>');
+}, 'document.writeln of another document must not throw an InvalidStateError when processing custom element reactions for a synchronous constructed custom element');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/upgrading.html b/testing/web-platform/tests/custom-elements/upgrading.html
new file mode 100644
index 0000000000..9a28fcbe12
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/upgrading.html
@@ -0,0 +1,258 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: Enqueue a custom element upgrade reaction</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="Enqueue a custom element upgrade reaction must upgrade a custom element">
+<link rel="help" href="https://dom.spec.whatwg.org/#concept-create-element">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/scripting.html#concept-try-upgrade">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/scripting.html#enqueue-a-custom-element-upgrade-reaction">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/custom-elements-helpers.js"></script>
+</head>
+<body>
+<infinite-cloning-element-1></infinite-cloning-element-1>
+<infinite-cloning-element-2 id="a"></infinite-cloning-element-2>
+<infinite-cloning-element-2 id="b"></infinite-cloning-element-2>
+<div id="log"></div>
+<script>
+setup({allow_uncaught_exception:true});
+
+class PredefinedCustomElement extends HTMLElement {}
+customElements.define('predefined-custom-element', PredefinedCustomElement);
+
+var customElementNumber = 1;
+function generateNextCustomElementName() { return 'custom-' + customElementNumber++; }
+
+// Tests for documents without a browsing context.
+document_types().filter(function (entry) { return !entry.isOwner && !entry.hasBrowsingContext; }).forEach(function (entry) {
+ var documentName = entry.name;
+ var getDocument = entry.create;
+
+ promise_test(function () {
+ return getDocument().then(function (doc) {
+ assert_false(doc.createElement('predefined-custom-element') instanceof PredefinedCustomElement);
+ });
+ }, 'Creating an element in ' + documentName + ' must not enqueue a custom element upgrade reaction'
+ + ' because the document does not have a browsing context');
+
+ promise_test(function () {
+ var name = generateNextCustomElementName();
+ var unresolvedElement = document.createElement(name);
+
+ assert_equals(Object.getPrototypeOf(unresolvedElement), HTMLElement.prototype,
+ '[[Prototype]] internal slot of the unresolved custom element must be the HTMLElement prototype');
+
+ return getDocument().then(function (doc) {
+ var unresolvedElementInDoc = doc.createElement(name);
+ var prototype = (unresolvedElementInDoc.namespaceURI == 'http://www.w3.org/1999/xhtml' ? HTMLElement : Element).prototype;
+
+ assert_equals(Object.getPrototypeOf(unresolvedElementInDoc), prototype,
+ '[[Prototype]] internal slot of the unresolved custom element must be the ' + prototype.toString() + ' prototype');
+ var someCustomElement = class extends HTMLElement {};
+ customElements.define(name, someCustomElement);
+ assert_equals(Object.getPrototypeOf(unresolvedElementInDoc), prototype, '"define" must not upgrade a disconnected unresolved custom elements');
+ doc.documentElement.appendChild(unresolvedElementInDoc);
+ assert_equals(Object.getPrototypeOf(unresolvedElementInDoc), prototype,
+ 'Inserting an element into a document without a browsing context must not enqueue a custom element upgrade reaction');
+ });
+ }, 'Creating an element in ' + documentName + ' and inserting into the document must not enqueue a custom element upgrade reaction');
+
+ promise_test(function () {
+ var name = generateNextCustomElementName();
+ var unresolvedElement = document.createElement(name);
+
+ assert_equals(Object.getPrototypeOf(unresolvedElement), HTMLElement.prototype,
+ '[[Prototype]] internal slot of the unresolved custom element must be the HTMLElement prototype');
+
+ return getDocument().then(function (doc) {
+ var unresolvedElementInDoc = doc.createElement(name);
+ var prototype = (unresolvedElementInDoc.namespaceURI == 'http://www.w3.org/1999/xhtml' ? HTMLElement : Element).prototype;
+
+ assert_equals(Object.getPrototypeOf(unresolvedElementInDoc), prototype,
+ '[[Prototype]] internal slot of the unresolved custom element must be the ' + prototype.toString() + ' prototype');
+ var someCustomElement = class extends HTMLElement {};
+ customElements.define(name, someCustomElement);
+ assert_equals(Object.getPrototypeOf(unresolvedElementInDoc), prototype, '"define" must not upgrade a disconnected unresolved custom elements');
+ document.body.appendChild(unresolvedElementInDoc);
+
+ if (unresolvedElementInDoc.namespaceURI == 'http://www.w3.org/1999/xhtml') {
+ assert_equals(Object.getPrototypeOf(unresolvedElementInDoc), someCustomElement.prototype,
+ 'Inserting an element into a document with a browsing context must enqueue a custom element upgrade reaction');
+ } else {
+ assert_equals(Object.getPrototypeOf(unresolvedElementInDoc), prototype,
+ 'Looking up a custom element definition must return null if the element is not in the HTML namespace');
+ }
+ });
+ }, 'Creating an element in ' + documentName + ' and adopting back to a document with browsing context must enqueue a custom element upgrade reaction');
+
+});
+
+// Tests for documents with a browsing context.
+document_types().filter(function (entry) { return !entry.isOwner && entry.hasBrowsingContext; }).forEach(function (entry) {
+ var documentName = entry.name;
+ var getDocument = entry.create;
+
+ promise_test(function () {
+ return getDocument().then(function (doc) {
+ assert_false(doc.createElement('predefined-custom-element') instanceof PredefinedCustomElement);
+ });
+ }, 'Creating an element in ' + documentName + ' must not enqueue a custom element upgrade reaction if there is no matching definition');
+
+ promise_test(function () {
+ return getDocument().then(function (doc) {
+ var docWindow = doc.defaultView;
+ class DistinctPredefinedCustomElement extends docWindow.HTMLElement { };
+ docWindow.customElements.define('predefined-custom-element', DistinctPredefinedCustomElement);
+ assert_true(doc.createElement('predefined-custom-element') instanceof DistinctPredefinedCustomElement);
+ });
+ }, 'Creating an element in ' + documentName + ' must enqueue a custom element upgrade reaction if there is a matching definition');
+
+ promise_test(function () {
+ var unresolvedElement = document.createElement('unresolved-element');
+ return getDocument().then(function (doc) {
+ var docWindow = doc.defaultView;
+ class UnresolvedElement extends docWindow.HTMLElement { };
+ var unresolvedElementInDoc = doc.createElement('unresolved-element');
+
+ assert_equals(Object.getPrototypeOf(unresolvedElement), HTMLElement.prototype);
+ assert_equals(Object.getPrototypeOf(unresolvedElementInDoc), docWindow.HTMLElement.prototype);
+
+ docWindow.customElements.define('unresolved-element', UnresolvedElement);
+
+ assert_equals(Object.getPrototypeOf(unresolvedElement), HTMLElement.prototype);
+ assert_equals(Object.getPrototypeOf(unresolvedElementInDoc), docWindow.HTMLElement.prototype);
+
+ });
+ }, '"define" in ' + documentName + ' must not enqueue a custom element upgrade reaction on a disconnected unresolved custom element');
+
+ promise_test(function () {
+ var unresolvedElement = document.createElement('unresolved-element');
+ return getDocument().then(function (doc) {
+ var docWindow = doc.defaultView;
+ class UnresolvedElement extends docWindow.HTMLElement { };
+ var unresolvedElementInDoc = doc.createElement('unresolved-element');
+
+ assert_equals(Object.getPrototypeOf(unresolvedElement), HTMLElement.prototype);
+ assert_equals(Object.getPrototypeOf(unresolvedElementInDoc), docWindow.HTMLElement.prototype);
+
+ docWindow.customElements.define('unresolved-element', UnresolvedElement);
+ doc.documentElement.appendChild(unresolvedElementInDoc);
+
+ assert_equals(Object.getPrototypeOf(unresolvedElement), HTMLElement.prototype);
+ assert_equals(Object.getPrototypeOf(unresolvedElementInDoc), UnresolvedElement.prototype);
+ });
+ }, 'Inserting an unresolved custom element into ' + documentName + ' must enqueue a custom element upgrade reaction');
+
+ promise_test(function () {
+ var unresolvedElement = document.createElement('unresolved-element');
+ return getDocument().then(function (doc) {
+ var docWindow = doc.defaultView;
+ class UnresolvedElement extends docWindow.HTMLElement { };
+ var unresolvedElementInDoc = doc.createElement('unresolved-element');
+ doc.documentElement.appendChild(unresolvedElementInDoc);
+
+ assert_equals(Object.getPrototypeOf(unresolvedElement), HTMLElement.prototype);
+ assert_equals(Object.getPrototypeOf(unresolvedElementInDoc), docWindow.HTMLElement.prototype);
+
+ docWindow.customElements.define('unresolved-element', UnresolvedElement);
+
+ assert_equals(Object.getPrototypeOf(unresolvedElement), HTMLElement.prototype);
+ assert_equals(Object.getPrototypeOf(unresolvedElementInDoc), UnresolvedElement.prototype);
+ });
+ }, '"define" in ' + documentName + ' must enqueue a custom element upgrade reaction on a connected unresolved custom element');
+
+ promise_test(function () {
+ var unresolvedElement = document.createElement('unresolved-element');
+ return getDocument().then(function (doc) {
+ var docWindow = doc.defaultView;
+ class UnresolvedElement extends docWindow.HTMLElement { };
+ assert_false(unresolvedElement instanceof UnresolvedElement);
+ docWindow.customElements.define('unresolved-element', UnresolvedElement);
+ doc.adoptNode(unresolvedElement);
+ assert_false(unresolvedElement instanceof UnresolvedElement);
+ });
+ }, 'Adopting (and leaving disconnceted) an unresolved custom element into ' + documentName + ' must not enqueue a custom element upgrade reaction');
+
+ promise_test(function () {
+ var unresolvedElement = document.createElement('unresolved-element');
+ return getDocument().then(function (doc) {
+ var docWindow = doc.defaultView;
+ class UnresolvedElement extends docWindow.HTMLElement { };
+ assert_false(unresolvedElement instanceof UnresolvedElement);
+ docWindow.customElements.define('unresolved-element', UnresolvedElement);
+ doc.documentElement.appendChild(unresolvedElement);
+ assert_true(unresolvedElement instanceof UnresolvedElement);
+ });
+ }, 'Adopting and inserting an unresolved custom element into ' + documentName + ' must enqueue a custom element upgrade reaction');
+
+});
+
+test(() => {
+ class ShadowDisabledElement extends HTMLElement {
+ static get disabledFeatures() { return ['shadow']; }
+ }
+ let error = null;
+ window.addEventListener('error', e => { error = e.error; }, {once: true});
+ let element = document.createElement('shadow-disabled');
+ element.attachShadow({mode: 'open'});
+ customElements.define('shadow-disabled', ShadowDisabledElement);
+ customElements.upgrade(element);
+ assert_false(element instanceof ShadowDisabledElement,
+ 'Upgrading should fail.');
+ assert_true(error instanceof DOMException);
+ assert_equals(error.name, 'NotSupportedError');
+}, 'If definition\'s disable shadow is true and element\'s shadow root is ' +
+ 'non-null, then throw a "NotSupportedError" DOMException.');
+
+test(() => {
+ var log = [];
+
+ customElements.define('infinite-cloning-element-1',class extends HTMLElement {
+ constructor() {
+ super();
+ log.push([this, 'begin']);
+ // Potential infinite recursion:
+ customElements.upgrade(this);
+ log.push([this, 'end']);
+ }
+ });
+
+ assert_equals(log.length, 2);
+ const instance = document.querySelector("infinite-cloning-element-1");
+ assert_array_equals(log[0], [instance, 'begin']);
+ assert_array_equals(log[1], [instance, 'end']);
+}, 'Infinite constructor recursion with upgrade(this) should not be possible');
+
+test(() => {
+ var log = [];
+
+ customElements.define('infinite-cloning-element-2',class extends HTMLElement {
+ constructor() {
+ super();
+ log.push([this, 'begin']);
+ const b = document.querySelector("#b");
+ b.remove();
+ // While this constructor is running for "a", "b" is still
+ // undefined, and so inserting it into the document will enqueue a
+ // second upgrade reaction for "b" in addition to the one enqueued
+ // by defining x-foo.
+ document.body.appendChild(b);
+ log.push([this, 'end']);
+ }
+ });
+
+ assert_equals(log.length, 4);
+ const instanceA = document.querySelector("#a");
+ const instanceB = document.querySelector("#b");
+ assert_array_equals(log[0], [instanceA, 'begin']);
+ assert_array_equals(log[1], [instanceB, 'begin']);
+ assert_array_equals(log[2], [instanceB, 'end']);
+ assert_array_equals(log[3], [instanceA, 'end']);
+}, 'Infinite constructor recursion with appendChild should not be possible');
+
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/upgrading/Document-importNode-customized-builtins.html b/testing/web-platform/tests/custom-elements/upgrading/Document-importNode-customized-builtins.html
new file mode 100644
index 0000000000..91c9ea8aee
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/upgrading/Document-importNode-customized-builtins.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<link rel="help" href="https://dom.spec.whatwg.org/#dom-document-importnode">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<body>
+<script>
+test_with_window((w, doc) => {
+ class MyDiv extends HTMLDivElement {}
+ class MyDiv2 extends w.HTMLDivElement {}
+ customElements.define('my-div', MyDiv, { extends: 'div' });
+ w.customElements.define('my-div', MyDiv2, { extends: 'div' });
+
+ let original = document.createElement('div', { is: 'my-div' });
+ assert_true(original instanceof MyDiv);
+
+ let imported = doc.importNode(original);
+ assert_true(imported instanceof MyDiv2);
+}, 'built-in: document.importNode() should import custom elements successfully');
+
+test_with_window((w, doc) => {
+ class MyDiv2 extends w.HTMLDivElement {}
+ w.customElements.define('my-div2', MyDiv2, { extends: 'div' });
+
+ let original = document.createElement('div', { is: 'my-div2' });
+ assert_equals(original.constructor, HTMLDivElement);
+
+ let imported = doc.importNode(original);
+ assert_true(imported instanceof MyDiv2);
+}, 'built-in: document.importNode() should import "undefined" custom elements successfully');
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/upgrading/Document-importNode.html b/testing/web-platform/tests/custom-elements/upgrading/Document-importNode.html
new file mode 100644
index 0000000000..3da4ccf46a
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/upgrading/Document-importNode.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<link rel="help" href="https://dom.spec.whatwg.org/#dom-document-importnode">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+<body>
+<script>
+test_with_window((w, doc) => {
+ class MyElement extends HTMLElement {}
+ class MyElement2 extends w.HTMLElement {}
+ customElements.define('my-element', MyElement);
+ w.customElements.define('my-element', MyElement2);
+
+ let original = document.createElement('my-element');
+ assert_true(original instanceof MyElement);
+
+ let imported = doc.importNode(original);
+ assert_true(imported instanceof MyElement2);
+}, 'autonomous: document.importNode() should import custom elements successfully');
+
+test_with_window((w, doc) => {
+ class MyElement3 extends w.HTMLElement {}
+ w.customElements.define('my-element3', MyElement3);
+
+ let original = document.createElement('my-element3');
+ assert_equals(original.constructor, HTMLElement);
+
+ let imported = doc.importNode(original);
+ assert_true(imported instanceof MyElement3);
+}, 'autonomous: document.importNode() should import "undefined" custom elements successfully');
+</script>
+</body>
diff --git a/testing/web-platform/tests/custom-elements/upgrading/Node-cloneNode-customized-builtins.html b/testing/web-platform/tests/custom-elements/upgrading/Node-cloneNode-customized-builtins.html
new file mode 100644
index 0000000000..5e1122cc84
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/upgrading/Node-cloneNode-customized-builtins.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: Upgrading</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="Node.prototype.cloneNode should upgrade a custom element">
+<link rel="help" href="https://html.spec.whatwg.org/#upgrades">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+setup({allow_uncaught_exception:true});
+
+test(function () {
+ class MyDiv1 extends HTMLDivElement {};
+ class MyDiv2 extends HTMLDivElement {};
+ class MyDiv3 extends HTMLDivElement {};
+ customElements.define('my-div1', MyDiv1, { extends: 'div' });
+ customElements.define('my-div2', MyDiv2, { extends: 'div' });
+
+ let instance = document.createElement('div', { is: 'my-div1'});
+ assert_true(instance instanceof MyDiv1);
+ instance.setAttribute('is', 'my-div2');
+ let clone = instance.cloneNode(false);
+ assert_not_equals(instance, clone);
+ assert_true(clone instanceof MyDiv1,
+ 'A cloned custom element must be an instance of the custom element even with an inconsistent "is" attribute');
+
+ let instance3 = document.createElement('div', { is: 'my-div3'});
+ assert_false(instance3 instanceof MyDiv3);
+ instance3.setAttribute('is', 'my-div2');
+ let clone3 = instance3.cloneNode(false);
+ assert_not_equals(instance3, clone);
+ customElements.define('my-div3', MyDiv3, { extends: 'div' });
+ document.body.appendChild(instance3);
+ document.body.appendChild(clone3);
+ assert_true(instance3 instanceof MyDiv3,
+ 'An undefined element must be upgraded even with an inconsistent "is" attribute');
+ assert_true(clone3 instanceof MyDiv3,
+ 'A cloned undefined element must be upgraded even with an inconsistent "is" attribute');
+}, 'Node.prototype.cloneNode(false) must be able to clone as a customized built-in element when it has an inconsistent "is" attribute');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/upgrading/Node-cloneNode.html b/testing/web-platform/tests/custom-elements/upgrading/Node-cloneNode.html
new file mode 100644
index 0000000000..1a05e96964
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/upgrading/Node-cloneNode.html
@@ -0,0 +1,206 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: Upgrading</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="Node.prototype.cloneNode should upgrade a custom element">
+<link rel="help" href="https://html.spec.whatwg.org/#upgrades">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+setup({allow_uncaught_exception:true});
+
+test(function () {
+ class MyCustomElement extends HTMLElement {}
+ customElements.define('my-custom-element', MyCustomElement);
+
+ var instance = document.createElement('my-custom-element');
+ assert_true(instance instanceof HTMLElement);
+ assert_true(instance instanceof MyCustomElement);
+
+ var clone = instance.cloneNode(false);
+ assert_not_equals(instance, clone);
+ assert_true(clone instanceof HTMLElement,
+ 'A cloned custom element must be an instance of HTMLElement');
+ assert_true(clone instanceof MyCustomElement,
+ 'A cloned custom element must be an instance of the custom element');
+}, 'Node.prototype.cloneNode(false) must be able to clone a custom element');
+
+test(function () {
+ class AutonomousCustomElement extends HTMLElement {};
+ class IsCustomElement extends HTMLElement {};
+
+ customElements.define('autonomous-custom-element', AutonomousCustomElement);
+ customElements.define('is-custom-element', IsCustomElement);
+
+ var instance = document.createElement('autonomous-custom-element', { is: "is-custom-element"});
+ assert_true(instance instanceof HTMLElement);
+ assert_true(instance instanceof AutonomousCustomElement);
+
+ var clone = instance.cloneNode(false);
+ assert_not_equals(instance, clone);
+ assert_true(clone instanceof HTMLElement,
+ 'A cloned custom element must be an instance of HTMLElement');
+ assert_true(clone instanceof AutonomousCustomElement,
+ 'A cloned custom element must be an instance of the custom element');
+}, 'Node.prototype.cloneNode(false) must be able to clone as a autonomous custom element when it contains is attribute');
+
+test_with_window(function (contentWindow) {
+ var contentDocument = contentWindow.document;
+ class MyCustomElement extends contentWindow.HTMLElement {}
+ contentWindow.customElements.define('my-custom-element', MyCustomElement);
+
+ var instance = contentDocument.createElement('my-custom-element');
+ assert_true(instance instanceof contentWindow.HTMLElement);
+ assert_true(instance instanceof MyCustomElement);
+
+ var clone = instance.cloneNode(false);
+ assert_not_equals(instance, clone);
+ assert_true(clone instanceof contentWindow.HTMLElement,
+ 'A cloned custom element must be an instance of HTMLElement');
+ assert_true(clone instanceof MyCustomElement,
+ 'A cloned custom element must be an instance of the custom element');
+}, 'Node.prototype.cloneNode(false) must be able to clone a custom element inside an iframe');
+
+test_with_window(function (contentWindow) {
+ var contentDocument = contentWindow.document;
+ class MyCustomElement extends contentWindow.HTMLElement { }
+ contentWindow.customElements.define('my-custom-element', MyCustomElement);
+
+ var instance = contentDocument.createElement('my-custom-element');
+ var container = contentDocument.createElement('div');
+ container.appendChild(instance);
+
+ var containerClone = container.cloneNode(true);
+ assert_true(containerClone instanceof contentWindow.HTMLDivElement);
+
+ var clone = containerClone.firstChild;
+ assert_not_equals(instance, clone);
+ assert_true(clone instanceof contentWindow.HTMLElement,
+ 'A cloned custom element must be an instance of HTMLElement');
+ assert_true(clone instanceof MyCustomElement,
+ 'A cloned custom element must be an instance of the custom element');
+}, 'Node.prototype.cloneNode(true) must be able to clone a descendent custom element');
+
+test_with_window(function (contentWindow) {
+ var parentNodeInConstructor;
+ var previousSiblingInConstructor;
+ var nextSiblingInConstructor;
+ class MyCustomElement extends contentWindow.HTMLElement {
+ constructor() {
+ super();
+ parentNodeInConstructor = this.parentNode;
+ previousSiblingInConstructor = this.previousSibling;
+ nextSiblingInConstructor = this.nextSibling;
+ }
+ }
+ contentWindow.customElements.define('my-custom-element', MyCustomElement);
+
+ var contentDocument = contentWindow.document;
+ var instance = contentDocument.createElement('my-custom-element');
+ var siblingBeforeInstance = contentDocument.createElement('b');
+ var siblingAfterInstance = contentDocument.createElement('a');
+ var container = contentDocument.createElement('div');
+ container.appendChild(siblingBeforeInstance);
+ container.appendChild(instance);
+ container.appendChild(siblingAfterInstance);
+
+ var containerClone = container.cloneNode(true);
+
+ assert_equals(parentNodeInConstructor, containerClone,
+ 'An upgraded element must have its parentNode set before the custom element constructor is called');
+ assert_equals(previousSiblingInConstructor, containerClone.firstChild,
+ 'An upgraded element must have its previousSibling set before the custom element constructor is called');
+ assert_equals(nextSiblingInConstructor, containerClone.lastChild,
+ 'An upgraded element must have its nextSibling set before the custom element constructor is called');
+}, 'Node.prototype.cloneNode(true) must set parentNode, previousSibling, and nextSibling before upgrading custom elements');
+
+// The error reporting isn't clear yet when multiple globals involved in custom
+// element, see w3c/webcomponents#635, so using test_with_window is not a good
+// idea here.
+test(function () {
+ class MyCustomElement extends HTMLElement {
+ constructor(doNotCreateItself) {
+ super();
+ if (!doNotCreateItself)
+ new MyCustomElement(true);
+ }
+ }
+ customElements.define('my-custom-element-constructed-after-super', MyCustomElement);
+
+ var instance = new MyCustomElement(false);
+ var uncaughtError;
+ window.onerror = function (message, url, lineNumber, columnNumber, error) { uncaughtError = error; return true; }
+ instance.cloneNode(false);
+ assert_equals(uncaughtError.name, 'TypeError');
+}, 'HTMLElement constructor must throw an TypeError when the top of the construction stack is marked AlreadyConstructed'
+ + ' due to a custom element constructor constructing itself after super() call');
+
+test(function () {
+ class MyCustomElement extends HTMLElement {
+ constructor(doNotCreateItself) {
+ if (!doNotCreateItself)
+ new MyCustomElement(true);
+ super();
+ }
+ }
+ customElements.define('my-custom-element-constructed-before-super', MyCustomElement);
+
+ var instance = new MyCustomElement(false);
+ var uncaughtError;
+ window.onerror = function (message, url, lineNumber, columnNumber, error) { uncaughtError = error; return true; }
+ instance.cloneNode(false);
+ assert_equals(uncaughtError.name, 'TypeError');
+}, 'HTMLElement constructor must throw an TypeError when the top of the construction stack is marked AlreadyConstructed'
+ + ' due to a custom element constructor constructing itself before super() call');
+
+test(function () {
+ var returnSpan = false;
+ class MyCustomElement extends HTMLElement {
+ constructor() {
+ super();
+ if (returnSpan)
+ return document.createElement('span');
+ }
+ }
+ customElements.define('my-custom-element-return-another', MyCustomElement);
+
+ var instance = new MyCustomElement(false);
+ returnSpan = true;
+ var uncaughtError;
+ window.onerror = function (message, url, lineNumber, columnNumber, error) { uncaughtError = error; return true; }
+ instance.cloneNode(false);
+ assert_equals(uncaughtError.name, 'TypeError');
+}, 'Upgrading a custom element must throw TypeError when the custom element\'s constructor returns another element');
+
+test(function () {
+ var instance = document.createElement('my-custom-element-throw-exception');
+ document.body.appendChild(instance);
+
+ var calls = [];
+ class MyCustomElement extends HTMLElement {
+ constructor() {
+ super();
+ calls.push(this);
+ throw 'bad';
+ }
+ }
+
+ var uncaughtError;
+ window.onerror = function (message, url, lineNumber, columnNumber, error) { uncaughtError = error; return true; }
+ customElements.define('my-custom-element-throw-exception', MyCustomElement);
+ assert_equals(uncaughtError, 'bad');
+
+ assert_array_equals(calls, [instance]);
+ document.body.removeChild(instance);
+ document.body.appendChild(instance);
+ assert_array_equals(calls, [instance]);
+}, 'Inserting an element must not try to upgrade a custom element when it had already failed to upgrade once');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/upgrading/upgrading-enqueue-reactions.html b/testing/web-platform/tests/custom-elements/upgrading/upgrading-enqueue-reactions.html
new file mode 100644
index 0000000000..8238eee624
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/upgrading/upgrading-enqueue-reactions.html
@@ -0,0 +1,158 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: Upgrading custom elements should enqueue attributeChanged and connected callbacks</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="Upgrading custom elements should enqueue attributeChanged and connected callbacksml">
+<meta name="help" content="https://html.spec.whatwg.org/#upgrades">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/custom-elements-helpers.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+setup({allow_uncaught_exception:true});
+
+test_with_window(function (contentWindow) {
+ const contentDocument = contentWindow.document;
+ contentDocument.write('<test-element id="some" title="This is a test">');
+
+ const undefinedElement = contentDocument.querySelector('test-element');
+ assert_equals(Object.getPrototypeOf(undefinedElement), contentWindow.HTMLElement.prototype);
+
+ let log = [];
+ class TestElement extends contentWindow.HTMLElement {
+ constructor() {
+ super();
+ log.push(create_constructor_log(this));
+ }
+ attributeChangedCallback(...args) {
+ log.push(create_attribute_changed_callback_log(this, ...args));
+ }
+ static get observedAttributes() { return ['id', 'title']; }
+ }
+ contentWindow.customElements.define('test-element', TestElement);
+ assert_equals(Object.getPrototypeOf(undefinedElement), TestElement.prototype);
+
+ assert_equals(log.length, 3);
+ assert_constructor_log_entry(log[0], undefinedElement);
+ assert_attribute_log_entry(log[1], {name: 'id', oldValue: null, newValue: 'some', namespace: null});
+ assert_attribute_log_entry(log[2], {name: 'title', oldValue: null, newValue: 'This is a test', namespace: null});
+}, 'Upgrading a custom element must enqueue attributeChangedCallback on each attribute');
+
+test_with_window(function (contentWindow) {
+ const contentDocument = contentWindow.document;
+ contentDocument.write('<test-element id="some" title="This is a test" class="foo">');
+
+ const undefinedElement = contentDocument.querySelector('test-element');
+ assert_equals(Object.getPrototypeOf(undefinedElement), contentWindow.HTMLElement.prototype);
+
+ let log = [];
+ class TestElement extends contentWindow.HTMLElement {
+ constructor() {
+ super();
+ log.push(create_constructor_log(this));
+ }
+ attributeChangedCallback(...args) {
+ log.push(create_attribute_changed_callback_log(this, ...args));
+ }
+ static get observedAttributes() { return ['class', 'id']; }
+ }
+ contentWindow.customElements.define('test-element', TestElement);
+ assert_equals(Object.getPrototypeOf(undefinedElement), TestElement.prototype);
+
+ assert_equals(log.length, 3);
+ assert_constructor_log_entry(log[0], undefinedElement);
+ assert_attribute_log_entry(log[1], {name: 'id', oldValue: null, newValue: 'some', namespace: null});
+ assert_attribute_log_entry(log[2], {name: 'class', oldValue: null, newValue: 'foo', namespace: null});
+}, 'Upgrading a custom element not must enqueue attributeChangedCallback on unobserved attributes');
+
+test_with_window(function (contentWindow) {
+ const contentDocument = contentWindow.document;
+ contentDocument.write('<test-element id="some" title="This is a test" class="foo">');
+
+ const undefinedElement = contentDocument.querySelector('test-element');
+ assert_equals(Object.getPrototypeOf(undefinedElement), contentWindow.HTMLElement.prototype);
+
+ let log = [];
+ class TestElement extends contentWindow.HTMLElement {
+ constructor() {
+ super();
+ log.push(create_constructor_log(this));
+ }
+ connectedCallback(...args) {
+ log.push(create_connected_callback_log(this, ...args));
+ }
+ }
+ contentWindow.customElements.define('test-element', TestElement);
+ assert_equals(Object.getPrototypeOf(undefinedElement), TestElement.prototype);
+
+ assert_equals(log.length, 2);
+ assert_constructor_log_entry(log[0], undefinedElement);
+ assert_connected_log_entry(log[1], undefinedElement);
+}, 'Upgrading a custom element must enqueue connectedCallback if the element in the document');
+
+test_with_window(function (contentWindow) {
+ const contentDocument = contentWindow.document;
+ contentDocument.write('<test-element id="some" title="This is a test" class="foo">');
+
+ const undefinedElement = contentDocument.querySelector('test-element');
+ assert_equals(Object.getPrototypeOf(undefinedElement), contentWindow.HTMLElement.prototype);
+
+ let log = [];
+ class TestElement extends contentWindow.HTMLElement {
+ constructor() {
+ super();
+ log.push(create_constructor_log(this));
+ }
+ connectedCallback(...args) {
+ log.push(create_connected_callback_log(this, ...args));
+ }
+ attributeChangedCallback(...args) {
+ log.push(create_attribute_changed_callback_log(this, ...args));
+ }
+ static get observedAttributes() { return ['class', 'id']; }
+ }
+ contentWindow.customElements.define('test-element', TestElement);
+ assert_equals(Object.getPrototypeOf(undefinedElement), TestElement.prototype);
+
+ assert_equals(log.length, 4);
+ assert_constructor_log_entry(log[0], undefinedElement);
+ assert_attribute_log_entry(log[1], {name: 'id', oldValue: null, newValue: 'some', namespace: null});
+ assert_attribute_log_entry(log[2], {name: 'class', oldValue: null, newValue: 'foo', namespace: null});
+ assert_connected_log_entry(log[3], undefinedElement);
+}, 'Upgrading a custom element must enqueue attributeChangedCallback before connectedCallback');
+
+test_with_window(function (contentWindow) {
+ const contentDocument = contentWindow.document;
+ contentDocument.write('<test-element id="some" title="This is a test" class="foo">');
+
+ const undefinedElement = contentDocument.querySelector('test-element');
+ assert_equals(Object.getPrototypeOf(undefinedElement), contentWindow.HTMLElement.prototype);
+
+ let log = [];
+ class TestElement extends contentWindow.HTMLElement {
+ constructor() {
+ super();
+ log.push(create_constructor_log(this));
+ throw 'Exception thrown as a part of test';
+ }
+ connectedCallback(...args) {
+ log.push(create_connected_callback_log(this, ...args));
+ }
+ attributeChangedCallback(...args) {
+ log.push(create_attribute_changed_callback_log(this, ...args));
+ }
+ static get observedAttributes() { return ['class', 'id']; }
+ }
+ contentWindow.customElements.define('test-element', TestElement);
+ assert_equals(Object.getPrototypeOf(undefinedElement), TestElement.prototype);
+
+ assert_equals(log.length, 1);
+ assert_constructor_log_entry(log[0], undefinedElement);
+}, 'Upgrading a custom element must not invoke attributeChangedCallback and connectedCallback when the element failed to upgrade');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/upgrading/upgrading-parser-created-element.html b/testing/web-platform/tests/custom-elements/upgrading/upgrading-parser-created-element.html
new file mode 100644
index 0000000000..0f7f95786d
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/upgrading/upgrading-parser-created-element.html
@@ -0,0 +1,125 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Custom Elements: Upgrading unresolved elements</title>
+<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
+<meta name="assert" content="HTML parser must add an unresolved custom element to the upgrade candidates map">
+<link rel="help" href="https://html.spec.whatwg.org/#upgrades">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<my-custom-element></my-custom-element>
+<instantiates-itself-after-super></instantiates-itself-after-super>
+<instantiates-itself-before-super></instantiates-itself-before-super>
+<my-other-element id="instance"></my-other-element>
+<my-other-element id="otherInstance"></my-other-element>
+<not-an-element></not-an-element>
+<not-an-html-element></not-an-html-element>
+<script>
+
+setup({allow_uncaught_exception:true});
+
+test(function () {
+ class MyCustomElement extends HTMLElement { }
+
+ var instance = document.querySelector('my-custom-element');
+ assert_true(instance instanceof HTMLElement);
+ assert_false(instance instanceof HTMLUnknownElement,
+ 'an unresolved custom element should not be an instance of HTMLUnknownElement');
+ assert_false(instance instanceof MyCustomElement);
+
+ customElements.define('my-custom-element', MyCustomElement);
+
+ assert_true(instance instanceof HTMLElement);
+ assert_true(instance instanceof MyCustomElement,
+ 'Calling customElements.define must upgrade existing custom elements');
+
+}, 'Element.prototype.createElement must add an unresolved custom element to the upgrade candidates map');
+
+test(function () {
+ class InstantiatesItselfAfterSuper extends HTMLElement {
+ constructor(doNotCreateItself) {
+ super();
+ if (!doNotCreateItself)
+ new InstantiatesItselfAfterSuper(true);
+ }
+ }
+
+ var uncaughtError;
+ window.onerror = function (message, url, lineNumber, columnNumber, error) { uncaughtError = error; return true; }
+ customElements.define('instantiates-itself-after-super', InstantiatesItselfAfterSuper);
+ assert_equals(uncaughtError.name, 'TypeError');
+}, 'HTMLElement constructor must throw an TypeError when the top of the construction stack is marked AlreadyConstructed'
+ + ' due to a custom element constructor constructing itself after super() call');
+
+test(function () {
+ class InstantiatesItselfBeforeSuper extends HTMLElement {
+ constructor(doNotCreateItself) {
+ if (!doNotCreateItself)
+ new InstantiatesItselfBeforeSuper(true);
+ super();
+ }
+ }
+
+ var uncaughtError;
+ window.onerror = function (message, url, lineNumber, columnNumber, error) { uncaughtError = error; return true; }
+ customElements.define('instantiates-itself-before-super', InstantiatesItselfBeforeSuper);
+ assert_equals(uncaughtError.name, 'TypeError');
+}, 'HTMLElement constructor must throw an TypeError when the top of the construction stack is marked AlreadyConstructed'
+ + ' due to a custom element constructor constructing itself before super() call');
+
+test(function () {
+ class MyOtherElement extends HTMLElement {
+ constructor() {
+ super();
+ if (this == instance)
+ return otherInstance;
+ }
+ }
+ var instance = document.getElementById('instance');
+ var otherInstance = document.getElementById('otherInstance');
+
+ assert_false(instance instanceof MyOtherElement);
+ assert_false(otherInstance instanceof MyOtherElement);
+
+ var uncaughtError;
+ window.onerror = function (message, url, lineNumber, columnNumber, error) { uncaughtError = error; return true; }
+ customElements.define('my-other-element', MyOtherElement);
+ assert_equals(uncaughtError.name, 'TypeError');
+
+ assert_true(document.createElement('my-other-element') instanceof MyOtherElement,
+ 'Upgrading of custom elements must happen after the definition was added to the registry.');
+
+}, 'Upgrading a custom element must throw an TypeError when the returned element is not SameValue as the upgraded element');
+
+test(() => {
+ class NotAnElement extends HTMLElement {
+ constructor() {
+ return new Text();
+ }
+ }
+
+ let uncaughtError;
+ window.onerror = function (message, url, lineNumber, columnNumber, error) { uncaughtError = error; return true; }
+ customElements.define("not-an-element", NotAnElement);
+ assert_equals(uncaughtError.name, "TypeError");
+}, "Upgrading a custom element whose constructor returns a Text node must throw");
+
+test(() => {
+ class NotAnHTMLElement extends HTMLElement {
+ constructor() {
+ return document.createElementNS("", "test");
+ }
+ }
+
+ let uncaughtError;
+ window.onerror = function (message, url, lineNumber, columnNumber, error) { uncaughtError = error; return true; }
+ customElements.define("not-an-html-element", NotAnHTMLElement);
+ assert_equals(uncaughtError.name, "TypeError");
+}, "Upgrading a custom element whose constructor returns an Element must throw");
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/custom-elements/when-defined-reentry-crash.html b/testing/web-platform/tests/custom-elements/when-defined-reentry-crash.html
new file mode 100644
index 0000000000..38614cbbd7
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/when-defined-reentry-crash.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Check for crashes when a whenDefined promise resolving re-entries</title>
+<meta name="author" href="mailto:xiaochengh@chromium.org">
+<link rel="help" href="https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-api">
+<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1366813">
+<script>
+class CustomElement extends HTMLElement {}
+
+Object.prototype.__defineGetter__("then", main);
+
+let depth = 0;
+function main() {
+ if (depth > 1) return;
+ ++depth;
+ customElements.whenDefined("custom-a"); // Causes re-entry of main()
+ try { customElements.define("custom-a", CustomElement) } catch (e) {}
+ customElements.whenDefined("custom-b");
+ --depth;
+}
+
+main();
+</script>
+
+Test passes if it does not crash.
diff --git a/testing/web-platform/tests/custom-elements/xhtml-crash.xhtml b/testing/web-platform/tests/custom-elements/xhtml-crash.xhtml
new file mode 100644
index 0000000000..8b45d2ecc8
--- /dev/null
+++ b/testing/web-platform/tests/custom-elements/xhtml-crash.xhtml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <script>
+ customElements.define('custom-el-1',
+ class extends HTMLElement {
+ constructor () {
+ super()
+ this.attachShadow({ mode: 'open' })
+ this.shadowRoot.innerHTML = '&lt;span/>'
+ }
+ })
+ </script>
+</head>
+<body>
+<custom-el-1/>