diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /testing/web-platform/tests/custom-elements | |
parent | Initial commit. (diff) | |
download | firefox-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')
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"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 = '<span/>' + } + }) + </script> +</head> +<body> +<custom-el-1/> |