mirror of
https://github.com/lexmount/moli.git
synced 2026-10-04 16:00:47 +00:00
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:
@@ -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/");
|
||||
|
||||
Reference in New Issue
Block a user