fix(custom-elements): preserve prototypes on failures before super

Only finalize failed upgrade wrappers if the construction stack shows that HTMLElement construction consumed the pending wrapper. Throwing or returning a different object before super must leave the original prototype intact.

Add main-window and iframe regressions for both failure paths, including discarded attribute and connected reactions.
This commit is contained in:
ldm0
2026-09-16 14:59:42 +08:00
committed by Donough Liu
parent 1b214ad088
commit 9035ba2f5d
2 changed files with 70 additions and 7 deletions
@@ -31,7 +31,11 @@ pub(super) fn upgrade_existing_custom_element_with_constructor<'s>(
.custom_elements_mut_for_node_handle(handle)
.begin_construction(scope, constructor, wrapper, handle);
let _dynamic_markup = enter_upgrade_dynamic_markup_insertion(host_ptr, handle);
match invoke_custom_element_constructor(scope, host_ptr, constructor) {
let invocation = invoke_custom_element_constructor(scope, host_ptr, constructor);
let consumed_pending_wrapper = unsafe { &*host_ptr }
.custom_elements_for_node_handle(handle)
.is_some_and(|store| store.pending_construction_is_already_constructed(handle));
match invocation {
CustomElementConstructorInvocation::Created(created) => {
let created = v8::Local::new(scope, &created);
if !validate_existing_custom_element_upgrade_result(wrapper, created) {
@@ -45,7 +49,9 @@ pub(super) fn upgrade_existing_custom_element_with_constructor<'s>(
),
failure_prototype,
);
finalize_wrapper_custom_element_prototype(scope, wrapper, constructor);
if consumed_pending_wrapper {
finalize_wrapper_custom_element_prototype(scope, wrapper, constructor);
}
return true;
}
finalize_wrapper_custom_element_prototype(scope, wrapper, constructor);
@@ -53,10 +59,13 @@ pub(super) fn upgrade_existing_custom_element_with_constructor<'s>(
true
}
CustomElementConstructorInvocation::Exception(exception) => {
finalize_wrapper_custom_element_prototype(scope, wrapper, constructor);
if let Some(canonical) = unsafe { &mut *host_ptr }
.native_bridge_mut()
.wrap_handle(scope, host_ptr, handle)
if consumed_pending_wrapper {
finalize_wrapper_custom_element_prototype(scope, wrapper, constructor);
}
if consumed_pending_wrapper
&& let Some(canonical) = unsafe { &mut *host_ptr }
.native_bridge_mut()
.wrap_handle(scope, host_ptr, handle)
{
if canonical.strict_equals(wrapper.into()) {
synchronize_custom_element_prototype_between_wrappers(
@@ -90,7 +99,9 @@ pub(super) fn upgrade_existing_custom_element_with_constructor<'s>(
ConstructionFailure::Exception(exception),
failure_prototype,
);
finalize_wrapper_custom_element_prototype(scope, wrapper, constructor);
if consumed_pending_wrapper {
finalize_wrapper_custom_element_prototype(scope, wrapper, constructor);
}
true
}
CustomElementConstructorInvocation::Empty => {
@@ -6889,6 +6889,58 @@ fn custom_element_constructor_error_reports_to_definition_window() {
);
}
#[test]
fn failed_existing_upgrade_before_super_keeps_original_prototype() {
let mut vm = new_storage_test_vm("https://ce-before-super.test/");
let result = vm.eval(r#"
(() => {
window.addEventListener("error", event => event.preventDefault());
const html = document.documentElement || document.appendChild(document.createElement("html"));
const body = document.body || html.appendChild(document.createElement("body"));
const frame = body.appendChild(document.createElement("iframe"));
const results = [];
for (const [realm, w] of [["main", window], ["child", frame.contentWindow]]) {
for (const failure of ["throw", "return-object"]) {
const doc = w.document;
const name = `before-super-${failure}`;
const element = doc.createElement(name);
element.setAttribute("data-value", "before");
(doc.body || doc.documentElement || doc).appendChild(element);
const original = Object.getPrototypeOf(element);
const log = [];
class FailedBeforeSuper extends w.HTMLElement {
constructor() {
log.push("constructor");
if (failure === "throw") throw new Error("before super");
return {};
}
static get observedAttributes() { return ["data-value"]; }
attributeChangedCallback() { log.push("attribute"); }
connectedCallback() { log.push("connected"); }
}
w.customElements.define(name, FailedBeforeSuper);
results.push({realm, failure,
unchanged: Object.getPrototypeOf(element) === original,
custom: element instanceof FailedBeforeSuper, log});
}
}
return JSON.stringify(results);
})()
"#).expect("failed upgrades before super should report their original prototypes");
let results: serde_json::Value = serde_json::from_str(&result).expect("upgrade results");
let results = results.as_array().expect("four upgrade cases");
assert_eq!(results.len(), 4);
for result in results {
assert_eq!(result["unchanged"], true, "{result}");
assert_eq!(result["custom"], false, "{result}");
assert_eq!(
result["log"],
serde_json::json!(["constructor"]),
"{result}"
);
}
}
#[test]
fn failed_existing_upgrade_preserves_definition_prototype_and_clears_reactions() {
let mut vm = new_storage_test_vm("https://ce-failed-upgrade.test/");