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