Skip to content

Commit

Permalink
Add tests for form-associated custom elements
Browse files Browse the repository at this point in the history
This is for WICG/webcomponents#187.

Specification PR: whatwg/html#4383.
  • Loading branch information
tkent-google authored and domenic committed May 16, 2019
1 parent 1de2d96 commit 3ccd79e
Show file tree
Hide file tree
Showing 9 changed files with 927 additions and 1 deletion.
99 changes: 98 additions & 1 deletion custom-elements/CustomElementRegistry.html
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,8 @@
assert_array_equals(prototypeCalls, [1, 'connectedCallback', 2, 'disconnectedCallback', 3, 'adoptedCallback', 4, 'attributeChangedCallback']);
assert_array_equals(constructorCalls, [0, 'prototype',
5, 'observedAttributes',
6, 'disabledFeatures']);
6, 'disabledFeatures',
7, 'formAssociated']);
}, 'customElements.define must get "observedAttributes" property on the constructor prototype when "attributeChangedCallback" is present');

test(function () {
Expand Down Expand Up @@ -437,6 +438,102 @@
assert_throws({'name': '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 proxy = new Proxy(constructor, {
get: function (target, name) {
calls.push(name);
if (name == 'formAssociated')
throw {name: 'expectedError'};
return target[name];
}
});
assert_throws({'name': 'expectedError'},
() => 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 = [];
constructor.prototype = new Proxy(constructor.prototype, {
get: function (target, name) {
calls.push(name);
if (name == 'formDisabledCallback')
throw {name: 'expectedError'};
return target[name];
}
});
assert_throws({'name': 'expectedError'},
() => 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({'name': '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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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('NotSupportedError', () => i.setFormValue(''));
assert_throws('NotSupportedError', () => i.form);
assert_throws('NotSupportedError', () => i.setValidity({}));
assert_throws('NotSupportedError', () => i.willValidate);
assert_throws('NotSupportedError', () => i.validity);
assert_throws('NotSupportedError', () => i.validationMessage);
assert_throws('NotSupportedError', () => i.checkValidity());
assert_throws('NotSupportedError', () => i.reportValidity());
assert_throws('NotSupportedError', () => i.labels);
}, 'Form-related operations and attributes should throw NotSupportedErrors' +
' for non-form-associated custom elements.');
</script>
</body>
50 changes: 50 additions & 0 deletions custom-elements/form-associated/ElementInternals-labels.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<!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);
}, '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');
</script>
</body>
125 changes: 125 additions & 0 deletions custom-elements/form-associated/ElementInternals-setFormValue.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<!DOCTYPE html>
<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) {
return new Promise((resolve, reject) => {
const iframe = $('iframe');
iframe.onload = () => resolve(iframe.contentWindow.location.search);
iframe.onerror = () => reject(new Error('iframe onerror fired'));
$('form').submit();
});
}

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-ce1=&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&name-ce2=');
});
}, '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');
</script>
Loading

0 comments on commit 3ccd79e

Please sign in to comment.