fix(events): compile popup content handlers in their owner scope

Resolve popup owners through the Window execution-context registry and
compile content attributes with the popup Window, Document, form, and
element scopes. Capture the popup owner when registering callbacks so
tasks retire with the popup even when compilation starts in the opener.

Route compile errors to the popup and preserve handler replacements made
while reporting them. Cover scope lookup, cancellation, lazy compile
errors, reentry, and timer retirement; refresh the observed WPT results.
This commit is contained in:
ldm0
2026-09-23 00:14:09 +08:00
parent d9584d90a2
commit 7d64c21b45
7 changed files with 196 additions and 32 deletions
@@ -2733,6 +2733,7 @@ html/browsers/history/the-location-interface/assign_before_load.html
html/browsers/history/the-location-interface/location_hash.html
html/browsers/history/the-location-interface/reload_document_write_onload.html
html/browsers/history/the-location-interface/same-hash.html
html/browsers/history/the-location-interface/scripted_click_assign_during_load.html
html/browsers/origin/cross-origin-objects/window-location-and-location-href-cross-realm-set.html
html/browsers/origin/relaxing-the-same-origin-restriction/document_domain_setter.html
html/browsers/the-window-object/accessing-other-browsing-contexts/indexed-browsing-contexts-02.html
@@ -6441,6 +6441,7 @@ html/browsers/history/the-location-interface/location_search.html
html/browsers/history/the-location-interface/no-browsing-context.window.js?moli-wpt-script=window
html/browsers/history/the-location-interface/reload_document_write.html
html/browsers/history/the-location-interface/replace-with-nested-iframe.html
html/browsers/history/the-location-interface/scripted_click_location_assign_during_load.html
html/browsers/history/the-location-interface/security_location_0.htm
html/browsers/origin/cross-origin-objects/cross-origin-due-to-document-domain-only.html
html/browsers/origin/cross-origin-objects/cross-origin-objects-function-caching.html
@@ -8216,6 +8217,7 @@ html/webappapis/scripting/event-loops/update-the-rendering-resize-autofocus.html
html/webappapis/scripting/events/body-onload.html
html/webappapis/scripting/events/compile-event-handler-lexical-scopes-form-owner.html
html/webappapis/scripting/events/compile-event-handler-lexical-scopes.html
html/webappapis/scripting/events/compile-event-handler-settings-objects.html
html/webappapis/scripting/events/compile-event-handler-symbol-unscopables.html
html/webappapis/scripting/events/event-handler-all-global-events.html
html/webappapis/scripting/events/event-handler-attributes-body-alt.html
@@ -91,8 +91,6 @@ html/browsers/history/the-history-interface/traverse_the_history_write_onload_1.
html/browsers/history/the-history-interface/traverse_the_history_write_onload_2.html
html/browsers/history/the-location-interface/location_replace_session_history.html
html/browsers/history/the-location-interface/reload_document_open_write.html
html/browsers/history/the-location-interface/scripted_click_assign_during_load.html
html/browsers/history/the-location-interface/scripted_click_location_assign_during_load.html
html/browsers/history/the-session-history-of-browsing-contexts/navigation-in-onload.html
html/browsers/sandboxing/sandbox-document-open.html
html/browsers/sandboxing/sandbox-window-open-srcdoc.html
@@ -153,7 +151,6 @@ html/semantics/scripting-1/the-script-element/execution-timing/102.html
html/semantics/scripting-1/the-script-element/microtasks/checkpoint-after-workerglobalscope-onerror-module.html
html/semantics/text-level-semantics/the-a-element/a-type.historical.html
html/webappapis/scripting/event-loops/new-scroll-event-dispatched-at-next-updating-rendering-time.html
html/webappapis/scripting/events/compile-event-handler-settings-objects.html
html/webappapis/scripting/events/onerroreventhandler.html
html/webappapis/scripting/processing-model-2/unhandled-promise-rejections/promise-rejection-events.serviceworker.https.html
import-maps/data-driven/resolving.html?data-url-prefix.json
@@ -136,7 +136,10 @@ pub(crate) fn dispatch_window_error_event_with_details<'s>(
};
let global = scope.get_current_context().global(scope);
ensure_window_reflecting_body_onerror_handler(scope);
let popup_id = crate::native_bridge::active_lightweight_popup_id(scope);
if popup_id.is_none() {
ensure_window_reflecting_body_onerror_handler(scope);
}
let error_value = error_value.unwrap_or_else(|| v8::null(scope).into());
let message = v8_string(scope, message)
@@ -173,6 +176,10 @@ pub(crate) fn dispatch_window_error_event_with_details<'s>(
mark_event_trusted(scope, event);
let runtime = unsafe { &mut *host_ptr };
if let Some(popup_id) = popup_id {
runtime.dispatch_lightweight_popup_window_event(scope, popup_id, "error", event);
return Ok(());
}
if let Some(child_handle) =
crate::context_bootstrap::child_browsing_context_handle_for_current_realm_scope(scope)
{
@@ -1,6 +1,7 @@
use crate::{
context_bootstrap::WINDOW_EVENT_HANDLER_PROPERTIES,
document_runtime::EventTargetHandle,
native_bridge::OwnerDispatchScope,
util::{
context_host_ptr_from_global_bridge, node_wrapper_from_handle, throw_type_error, v8_string,
v8str,
@@ -409,7 +410,9 @@ fn node_event_handler_getter_function<'s>(
rv.set(v8::null(scope).into());
return;
};
let Some(target_context) = node_event_handler_target_context(scope, runtime_ptr, handle) else {
let Some((target_context, dispatch_scope)) =
node_event_handler_target_context(scope, runtime_ptr, handle)
else {
rv.set(v8::null(scope).into());
return;
};
@@ -427,29 +430,23 @@ fn node_event_handler_getter_function<'s>(
return;
}
let handler = if target_context == scope.get_current_context() {
compile_node_event_attribute_handler(
scope,
runtime_ptr,
handle,
object,
&handler_name,
&source,
)
.map(|handler| v8::Global::new(scope, handler))
} else {
let handler = {
let object = v8::Global::new(scope, object);
let target_scope = &mut v8::ContextScope::new(scope, target_context);
let previous = dispatch_scope.enter(target_scope);
let object = v8::Local::new(target_scope, &object);
compile_node_event_attribute_handler(
let handler = compile_node_event_attribute_handler(
target_scope,
runtime_ptr,
dispatch_scope,
handle,
object,
&handler_name,
&source,
)
.map(|handler| v8::Global::new(target_scope, handler))
.map(|handler| v8::Global::new(target_scope, handler));
dispatch_scope.restore(target_scope, previous);
handler
};
match handler {
Some(handler) => rv.set(v8::Local::new(scope, &handler).into()),
@@ -461,25 +458,24 @@ fn node_event_handler_target_context<'s>(
scope: &mut v8::PinScope<'s, '_>,
runtime_ptr: *mut crate::native_bridge::JsContextHost,
handle: crate::document_runtime::DomHandle,
) -> Option<v8::Local<'s, v8::Context>> {
let runtime = unsafe { &*runtime_ptr };
let owner_document = runtime.dom_host().owner_document_handle(handle)?;
let dispatch_scope = if owner_document == runtime.dom_host().document_handle() {
crate::native_bridge::OwnerDispatchScope::Top
} else {
crate::native_bridge::OwnerDispatchScope::Child(
runtime.child_browsing_context_host_for_document_handle(owner_document)?,
)
};
) -> Option<(v8::Local<'s, v8::Context>, OwnerDispatchScope)> {
let runtime = unsafe { &mut *runtime_ptr };
let dispatch_scope = runtime.owner_dispatch_scope_for_node(handle)?;
if let OwnerDispatchScope::LightweightPopup(popup_id) = dispatch_scope
&& !runtime.ensure_lightweight_popup_execution_context(scope, popup_id)
{
return None;
}
let owner = runtime.current_window_execution_context_owner(dispatch_scope)?;
runtime
.window_execution_context(scope, owner, dispatch_scope)
.map(|(_, context)| context)
.map(|(_, context)| (context, dispatch_scope))
}
fn compile_node_event_attribute_handler<'s>(
scope: &mut v8::PinScope<'s, '_>,
runtime_ptr: *mut crate::native_bridge::JsContextHost,
dispatch_scope: OwnerDispatchScope,
handle: crate::document_runtime::DomHandle,
object: v8::Local<'s, v8::Object>,
handler_name: &str,
@@ -498,9 +494,20 @@ fn compile_node_event_attribute_handler<'s>(
target_context,
);
let event_argument = v8_string(scope, "event")?;
let global = scope.get_current_context().global(scope);
let mut context_extensions = Vec::with_capacity(3);
if let Some(document) = global
let mut context_extensions = Vec::with_capacity(4);
let window = match dispatch_scope {
OwnerDispatchScope::LightweightPopup(popup_id) => {
let window = unsafe { &*runtime_ptr }.lightweight_popup_window(scope, popup_id)?;
// Popup Windows currently share a concrete V8 realm with their
// opener. Their own global bindings must precede that realm's globals.
context_extensions.push(window);
window
}
OwnerDispatchScope::Top | OwnerDispatchScope::Child(_) => {
scope.get_current_context().global(scope)
}
};
if let Some(document) = window
.get(scope, v8str(scope, "document").into())
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
{
@@ -3,6 +3,7 @@ use super::*;
mod child_window;
mod compilation;
mod object_values;
mod popup;
#[test]
fn event_attribute_handlers_use_html_scope_chain_and_report_compile_errors() {
@@ -0,0 +1,149 @@
use super::*;
#[test]
fn popup_content_handlers_use_their_window_document_form_and_element_scopes() {
let mut vm = new_storage_test_vm("https://popup-content-handler.test/");
let result = vm
.eval(
r#"
(() => {
const popup = open();
try {
const d = popup.document;
d.body.innerHTML = '<form><button id="button" type="button"></button></form>';
const button = d.getElementById('button');
const form = button.form;
const trace = popup.__handlerTrace = [];
popup.scopeToken = 'window';
d.scopeToken = 'document';
form.scopeToken = 'form';
button.scopeToken = 'element';
button.setAttribute('onclick', `
globalThis.__handlerTrace.push([globalThis === window, window !== opener,
document === ownerDocument, form === document.forms[0],
this === document.getElementById('button'), scopeToken,
event.type, arguments.length]);
location.hash = 'handled';
return false;
`);
const handler = button.onclick;
const canceled = [];
for (const object of [button, form, d, popup]) {
canceled.push(!button.dispatchEvent(new Event('click', {cancelable:true})));
delete object.scopeToken;
}
return JSON.stringify({type:typeof handler, cached:handler === button.onclick,
trace, canceled, popupHash:popup.location.hash, openerHash:location.hash,
openerUntouched:!Object.hasOwn(window, '__handlerTrace')});
} finally {
popup.close();
}
})()
"#,
)
.expect("popup content handler scope probe should evaluate");
assert_eq!(
serde_json::from_str::<serde_json::Value>(&result).unwrap(),
serde_json::json!({
"type": "function",
"cached": true,
"trace": [
[true, true, true, true, true, "element", "click", 1],
[true, true, true, true, true, "form", "click", 1],
[true, true, true, true, true, "document", "click", 1],
[true, true, true, true, true, "window", "click", 1]
],
"canceled": [true, true, true, true],
"popupHash": "#handled",
"openerHash": "",
"openerUntouched": true
})
);
}
#[test]
fn popup_content_handler_compile_errors_report_to_the_popup_and_preserve_reentry() {
let mut vm = new_storage_test_vm("https://popup-content-handler-error.test/");
let result = vm
.eval(
r#"
(() => {
const popup = open();
try {
const button = popup.document.createElement('button');
popup.document.body.appendChild(button);
const trace = [];
let openerErrors = 0;
window.addEventListener('error', () => ++openerErrors);
const replacement = () => trace.push('replacement');
popup.addEventListener('error', event => {
trace.push([event.target === popup, event.error.name === 'SyntaxError',
button.onclick === null]);
button.onclick = replacement;
event.preventDefault();
});
button.setAttribute('onclick', '}');
const before = trace.length;
const firstNull = button.onclick === null;
const replacementPreserved = button.onclick === replacement;
button.click();
return JSON.stringify({before, firstNull, replacementPreserved, trace, openerErrors});
} finally {
popup.close();
}
})()
"#,
)
.expect("popup content handler syntax error probe should evaluate");
assert_eq!(
serde_json::from_str::<serde_json::Value>(&result).unwrap(),
serde_json::json!({
"before": 0,
"firstNull": true,
"replacementPreserved": true,
"trace": [[true, true, true], "replacement"],
"openerErrors": 0
})
);
}
#[tokio::test]
async fn popup_content_handler_timers_retire_with_the_popup() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
let mut vm = new_page_task_executor_test_vm_with_loader(
"https://popup-content-handler-timer.test/",
&loader,
);
assert_eq!(
vm.eval(
r#"
(() => {
globalThis.__popupContentTimerCalls = 0;
globalThis.__openerContentTimerDone = false;
const popup = open();
const button = popup.document.createElement('button');
popup.document.body.appendChild(button);
button.setAttribute('onclick', `
setTimeout(() => ++opener.__popupContentTimerCalls, 50);
`);
const compiled = typeof button.onclick;
button.click();
popup.close();
setTimeout(() => { __openerContentTimerDone = true; }, 100);
return compiled;
})()
"#,
)
.expect("popup content handler timer probe should evaluate"),
"function"
);
advance_page_task_executor_until_eval_equals(
&mut vm,
&loader,
"String(__openerContentTimerDone)",
"true",
"opener timer after popup close",
)
.await;
assert_eq!(vm.eval("String(__popupContentTimerCalls)").unwrap(), "0");
}