diff options
Diffstat (limited to 'testing/web-platform/tests/custom-elements/form-associated')
17 files changed, 1939 insertions, 0 deletions
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-notref.html b/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-reportValidity-bubble-notref.html new file mode 100644 index 0000000000..dda2332965 --- /dev/null +++ b/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-reportValidity-bubble-notref.html @@ -0,0 +1,26 @@ +<!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); +</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..adad00d90e --- /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=mismatch href=ElementInternals-reportValidity-bubble-notref.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-nullish-value.html b/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-setFormValue-nullish-value.html new file mode 100644 index 0000000000..28c12c4600 --- /dev/null +++ b/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-setFormValue-nullish-value.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<html> +<head> + <title>ElementInternals.setFormValue(nullish value) should clear submission value</title> + <link rel="help" href="https://html.spec.whatwg.org/multipage/custom-elements.html#dom-elementinternals-setformvalue"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script> + customElements.define("test-form-element", class extends HTMLElement { + static formAssociated = true; + constructor() { + super(); + this.i = this.attachInternals(); + } + }); + </script> +</head> +<body> + <form id="form-null"> + <test-form-element id="input-null" name="input-null"></test-form-element> + </form> + + <form id="form-undefined"> + <test-form-element id="input-undefined" name="input-undefined"></test-form-element> + </form> + + <script> + test(() => { + const input = document.getElementById("input-null"); + input.i.setFormValue("fail"); + input.i.setFormValue(null); + const formData = new FormData(document.getElementById("form-null")); + assert_false(formData.has("input-null")); + }, "ElementInternals.setFormValue(null) clears submission value"); + + test(() => { + const input = document.getElementById("input-undefined"); + input.i.setFormValue("fail"); + input.i.setFormValue(undefined); + const formData = new FormData(document.getElementById("form-undefined")); + assert_false(formData.has("input-undefined")); + }, "ElementInternals.setFormValue(undefined) clears submission value"); + </script> +</body> +</html> 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..714f929e06 --- /dev/null +++ b/testing/web-platform/tests/custom-elements/form-associated/ElementInternals-target-element-is-held-strongly.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<title>Target element of ElementsInternals is held strongly and doesn't get GCed if there are no other references</title> +<meta name="timeout" content="long"> +<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/focusability.html b/testing/web-platform/tests/custom-elements/form-associated/focusability.html new file mode 100644 index 0000000000..cc12b7d51b --- /dev/null +++ b/testing/web-platform/tests/custom-elements/form-associated/focusability.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<title>Focusability of form-associated custom elements</title> +<link rel="author" title="Michael[tm] Smith" href="mailto:mike@w3.org"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<x-foo></x-foo> +<x-bar tabindex=0></x-bar> +<x-baz tabindex=0 disabled></x-baz> +<script> + test(() => { + customElements.define( + 'x-foo', + class Foo extends HTMLElement { + static formAssociated = true; + } + ); + const foo = document.querySelector('x-foo'); + foo.focus(); + assert_false(document.activeElement === foo, + "FACE without tabindex is not focusable."); + customElements.define( + 'x-bar', + class bar extends HTMLElement { + static formAssociated = true; + } + ); + const bar = document.querySelector('x-bar'); + bar.focus(); + assert_true(document.activeElement === bar, + "FACE with tabindex is focusable."); + customElements.define( + 'x-baz', + class baz extends HTMLElement { + static formAssociated = true; + } + ); + const baz = document.querySelector('x-baz'); + baz.focus(); + assert_false(document.activeElement === baz, + "Disabled FACE with tabindex is not focusable."); + }); +</script> +</html> 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..a30b678f13 --- /dev/null +++ b/testing/web-platform/tests/custom-elements/form-associated/form-disabled-callback.html @@ -0,0 +1,154 @@ +<!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'); + +test(() => { + const template = document.createElement('template'); + template.innerHTML = '<my-control></my-control>'; + const container = document.createElement('fieldset'); + document.body.appendChild(container); + container.disabled = true; + container.appendChild(template.content.cloneNode(true)); + assert_array_equals(container.querySelector('my-control').disabledHistory(), [true]); +}, 'Callback triggered during a clone/append operation, with disabled state provided by ancestor'); + +test(() => { + const container = document.createElement('div'); + document.body.appendChild(container); + container.innerHTML = '<fieldset disabled><my-control></my-control></fieldset>'; + const clone = container.cloneNode(true); + assert_array_equals(container.querySelector('my-control').disabledHistory(), [true]); +}, 'Callback triggered during a clone operation, with disabled state provided by ancestor'); +</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> |