IDLGenerators: Throw TypeError if custom element is already constructed

Throwing a TypeError instead of an InvalidStateError DOMException
follows the current specification steps.
This commit is contained in:
Tim Ledbetter 2025-02-21 16:45:06 +00:00 committed by Tim Flynn
commit 7ee33529e8
Notes: github-actions[bot] 2025-02-22 10:40:27 +00:00
5 changed files with 358 additions and 2 deletions

View file

@ -2701,9 +2701,9 @@ static void generate_html_constructor(SourceGenerator& generator, IDL::Construct
// 10. Let element be the last entry in definition's construction stack. // 10. Let element be the last entry in definition's construction stack.
auto& element = definition->construction_stack().last(); auto& element = definition->construction_stack().last();
// 11. If element is an already constructed marker, then throw an "InvalidStateError" DOMException. // 11. If element is an already constructed marker, then throw a TypeError.
if (element.has<HTML::AlreadyConstructedCustomElementMarker>()) if (element.has<HTML::AlreadyConstructedCustomElementMarker>())
return JS::throw_completion(WebIDL::InvalidStateError::create(realm, "Custom element has already been constructed"_string)); return vm.throw_completion<JS::TypeError>("Custom element has already been constructed"sv);
// 12. Perform ? element.[[SetPrototypeOf]](prototype). // 12. Perform ? element.[[SetPrototypeOf]](prototype).
auto actual_element = element.get<GC::Ref<DOM::Element>>(); auto actual_element = element.get<GC::Ref<DOM::Element>>();

View file

@ -0,0 +1,14 @@
Harness status: OK
Found 9 tests
9 Pass
Pass Node.prototype.cloneNode(false) must be able to clone a custom element
Pass Node.prototype.cloneNode(false) must be able to clone as a autonomous custom element when it contains is attribute
Pass Node.prototype.cloneNode(false) must be able to clone a custom element inside an iframe
Pass Node.prototype.cloneNode(true) must be able to clone a descendent custom element
Pass Node.prototype.cloneNode(true) must set parentNode, previousSibling, and nextSibling before upgrading custom elements
Pass 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
Pass 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
Pass Upgrading a custom element must throw TypeError when the custom element's constructor returns another element
Pass Inserting an element must not try to upgrade a custom element when it had already failed to upgrade once

View file

@ -0,0 +1,11 @@
Harness status: OK
Found 6 tests
6 Pass
Pass Element.prototype.createElement must add an unresolved custom element to the upgrade candidates map
Pass 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
Pass 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
Pass Upgrading a custom element must throw an TypeError when the returned element is not SameValue as the upgraded element
Pass Upgrading a custom element whose constructor returns a Text node must throw
Pass Upgrading a custom element whose constructor returns an Element must throw

View file

@ -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>

View file

@ -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>