mirror of
https://github.com/lexmount/moli.git
synced 2026-10-30 16:07:24 +00:00
fix(renderer): project Node listeners and event targets into their world
This commit is contained in:
@@ -276,11 +276,11 @@ pub(crate) use self::events::{
|
||||
EVENT_DISPATCHING_SLOT, EVENT_PASSIVE_SLOT, EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT,
|
||||
EVENT_STOP_PROPAGATION_SLOT, clear_event_composed_path, construct_original_error_event,
|
||||
construct_original_message_event, event_attribute, event_backing, event_bool_attribute,
|
||||
event_initialized, event_internal_bool_flag, event_is_dispatching, event_is_error_event,
|
||||
event_is_mouse_event, event_private_value, initialize_event_object, initialize_event_wrapper,
|
||||
mark_agent_submit_event, mark_event_trusted, new_event_state, new_event_wrapper,
|
||||
set_event_composed_path, set_event_internal_flag, set_event_private_value, set_event_trusted,
|
||||
submit_event_submitter_value, window_event_callback_values,
|
||||
event_callback_values, event_initialized, event_internal_bool_flag, event_is_dispatching,
|
||||
event_is_error_event, event_is_mouse_event, event_private_value, initialize_event_object,
|
||||
initialize_event_wrapper, mark_agent_submit_event, mark_event_trusted, new_event_state,
|
||||
new_event_wrapper, set_event_composed_path, set_event_internal_flag, set_event_private_value,
|
||||
set_event_trusted, submit_event_submitter_value,
|
||||
};
|
||||
pub(crate) use self::file_api::{
|
||||
DataTransferStringCallbackTask, DataTransferStringCallbackTaskEffect,
|
||||
|
||||
@@ -496,8 +496,8 @@ pub(super) fn track_event_track_getter_function<'s>(
|
||||
}
|
||||
|
||||
pub(crate) use wrappers::{
|
||||
event_attribute, event_bool_attribute, event_private_value, initialize_event_wrapper,
|
||||
new_event_state, new_event_wrapper, set_event_private_value, window_event_callback_values,
|
||||
event_attribute, event_bool_attribute, event_callback_values, event_private_value,
|
||||
initialize_event_wrapper, new_event_state, new_event_wrapper, set_event_private_value,
|
||||
};
|
||||
|
||||
pub(in crate::context_bootstrap) use message::message_event_init_callback;
|
||||
|
||||
@@ -153,12 +153,12 @@ pub(crate) fn new_event_wrapper<'s>(
|
||||
Some(wrapper)
|
||||
}
|
||||
|
||||
/// Project another Realm of the same live Window through the existing weak
|
||||
/// Event cache. The dispatch state stays shared and same-world identity stays
|
||||
/// intact. Foreign-Window callbacks retain their existing behavior: resolving
|
||||
/// an isolated world of another target Window requires an alias/world resolver
|
||||
/// that this path does not yet provide.
|
||||
pub(crate) fn window_event_callback_values<'s>(
|
||||
/// Project DOM and Window listeners of the same live Window through their
|
||||
/// existing world caches. The dispatch state stays shared and same-world
|
||||
/// identity stays intact. Foreign-Window callbacks retain their existing
|
||||
/// behavior: resolving an isolated world of another target Window requires an
|
||||
/// alias/world resolver that this path does not yet provide.
|
||||
pub(crate) fn event_callback_values<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
receiver: v8::Local<'s, v8::Value>,
|
||||
event: v8::Local<'s, v8::Object>,
|
||||
@@ -167,9 +167,10 @@ pub(crate) fn window_event_callback_values<'s>(
|
||||
let Ok(target) = v8::Local::<v8::Object>::try_from(receiver) else {
|
||||
return Some((receiver, event));
|
||||
};
|
||||
let Some(target) =
|
||||
super::super::shared_event_targets::window_target_in_realm(scope, target, context)
|
||||
else {
|
||||
let Some(target) = super::super::shared_event_targets::window_target_in_realm(
|
||||
scope, target, context,
|
||||
)
|
||||
.or_else(|| super::super::shared_event_targets::node_target_in_realm(scope, target, context)) else {
|
||||
return Some((receiver, event));
|
||||
};
|
||||
if world_wrappers::belongs_to_world(scope, event, context) {
|
||||
|
||||
@@ -63,6 +63,9 @@ pub(super) fn target_in_realm<'s>(
|
||||
if let Some(window) = window_target_in_realm(scope, target, context) {
|
||||
return window;
|
||||
}
|
||||
if let Some(node) = node_target_in_realm(scope, target, context) {
|
||||
return node;
|
||||
}
|
||||
let Some(owner) = get_private_object(scope, target, OWNER) else {
|
||||
return target;
|
||||
};
|
||||
@@ -119,6 +122,35 @@ pub(super) fn window_target_in_realm<'s>(
|
||||
Some(context.global(scope))
|
||||
}
|
||||
|
||||
/// Select the listener world's wrapper only for a Node owned by this live
|
||||
/// Window. A wrapper's creation Context is not its native ownerDocument:
|
||||
/// adoption and borrowed callbacks can make those belong to different Windows.
|
||||
pub(super) fn node_target_in_realm<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
target: v8::Local<'s, v8::Object>,
|
||||
context: v8::Local<'s, v8::Context>,
|
||||
) -> Option<v8::Local<'s, v8::Object>> {
|
||||
if !crate::web_api_interfaces::Node::is_instance(scope, target) {
|
||||
return None;
|
||||
}
|
||||
let (host_ptr, handle) =
|
||||
crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, target)
|
||||
.ok()?;
|
||||
if crate::util::context_host_ptr_from_context_slot(context) != Some(host_ptr) {
|
||||
return None;
|
||||
}
|
||||
let host = unsafe { &*host_ptr };
|
||||
let identity = host.window_execution_context_identity_for_access_check(context)?;
|
||||
let owner_scope = host.owner_dispatch_scope_for_node(handle)?;
|
||||
if identity.dispatch_scope() != owner_scope
|
||||
|| !host.window_execution_context_identity_is_current(identity)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
super::platform_object_worlds::in_realm(scope, target.into(), context)
|
||||
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
|
||||
}
|
||||
|
||||
pub(super) fn install_handlers<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
target: v8::Local<'s, v8::Object>,
|
||||
|
||||
@@ -156,15 +156,9 @@ pub(crate) fn invoke_prepared_event_callback<'s>(
|
||||
) -> Option<v8::Global<v8::Value>> {
|
||||
let receiver = event_target_receiver(scope, host_ptr, target, event);
|
||||
let has_event_argument = arguments.len() == 1 && arguments[0].strict_equals(event.into());
|
||||
let (receiver, event) = if matches!(
|
||||
target,
|
||||
EventTargetHandle::Window | EventTargetHandle::ChildWindow(_)
|
||||
) {
|
||||
let context = callback.relevant_context(scope);
|
||||
crate::context_bootstrap::window_event_callback_values(scope, receiver, event, context)?
|
||||
} else {
|
||||
(receiver, event)
|
||||
};
|
||||
let context = callback.relevant_context(scope);
|
||||
let (receiver, event) =
|
||||
crate::context_bootstrap::event_callback_values(scope, receiver, event, context)?;
|
||||
// Window.onerror's five arguments are JS values, not an Event conversion.
|
||||
let event_arguments = [event.into()];
|
||||
let arguments = if has_event_argument {
|
||||
|
||||
@@ -2197,6 +2197,7 @@ mod mouse_snapshot;
|
||||
mod native_bridge_identity;
|
||||
mod native_exception_stack_capture;
|
||||
mod no_cors_header_fill;
|
||||
mod node_event_worlds;
|
||||
mod observer_callbacks;
|
||||
mod observer_receivers;
|
||||
mod offline_audio_float;
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
use super::*;
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn node_event_callbacks_project_nodes_and_event_state_in_both_layout_modes() {
|
||||
for policy in [
|
||||
moli_page_types::LayoutPolicy::Mock,
|
||||
moli_page_types::LayoutPolicy::OnDemand,
|
||||
] {
|
||||
let mut vm = new_storage_test_vm("https://node-event-worlds.test/");
|
||||
vm.set_layout_policy(policy);
|
||||
vm.eval(
|
||||
r#"
|
||||
globalThis.button = document.createElement('button');
|
||||
button.id = 'probe';
|
||||
button.textContent = 'text';
|
||||
(document.body || document.documentElement || document).appendChild(button);
|
||||
globalThis.nodes = [button, button.firstChild, document];
|
||||
globalThis.defaultFacts = [];
|
||||
globalThis.originals = [];
|
||||
globalThis.payload = {token: 'payload'};
|
||||
nodes.forEach((node, index) => {
|
||||
node.defaultNodeOnly = true;
|
||||
node.addEventListener('node-world-' + index, function(event) {
|
||||
defaultFacts.push([
|
||||
this === node, event === originals[index], event instanceof CustomEvent,
|
||||
event.target === node, event.currentTarget === node,
|
||||
event.srcElement === node, event.composedPath()[0] === node
|
||||
]);
|
||||
});
|
||||
});
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let isolated = vm
|
||||
.create_isolated_world("node-event-listeners", false)
|
||||
.unwrap();
|
||||
vm.eval_in_isolated_context(isolated, r#"
|
||||
globalThis.nodes = [document.getElementById('probe'), document.getElementById('probe').firstChild, document];
|
||||
globalThis.facts = [];
|
||||
globalThis.objectFacts = [];
|
||||
globalThis.savedEvents = [];
|
||||
nodes.forEach((node, index) => {
|
||||
node.isolatedNodeOnly = true;
|
||||
node.addEventListener('node-world-' + index, function(event) {
|
||||
savedEvents[index] = event;
|
||||
facts.push([
|
||||
this === node, event instanceof CustomEvent,
|
||||
event.target === node, event.currentTarget === node,
|
||||
event.srcElement === node, event.composedPath()[0] === node,
|
||||
window.event === event, event.defaultOnly === undefined,
|
||||
event.target.defaultNodeOnly === undefined, event.detail.token === 'payload'
|
||||
]);
|
||||
event.preventDefault();
|
||||
});
|
||||
const listener = {handleEvent(event) {
|
||||
objectFacts.push([this === listener, event === savedEvents[index],
|
||||
event.defaultPrevented, event.currentTarget === node]);
|
||||
}};
|
||||
node.addEventListener('node-world-' + index, listener);
|
||||
});
|
||||
"#).unwrap();
|
||||
let builds = vm.layout_pass_observability_for_test().1;
|
||||
let result = vm.eval(r#"
|
||||
JSON.stringify(nodes.map((node, index) => {
|
||||
const event = new CustomEvent('node-world-' + index, {cancelable: true, detail: payload});
|
||||
event.defaultOnly = true;
|
||||
originals[index] = event;
|
||||
return [node.dispatchEvent(event) === false, event.defaultPrevented,
|
||||
event.detail === payload,
|
||||
event.currentTarget === null, event.composedPath().length === 0,
|
||||
node.isolatedNodeOnly === undefined];
|
||||
}));
|
||||
"#).unwrap();
|
||||
assert_eq!(
|
||||
serde_json::from_str::<serde_json::Value>(&result).unwrap(),
|
||||
serde_json::json!(vec![vec![true; 6]; 3]),
|
||||
"shared dispatch state and arbitrary JS payloads: {policy:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<serde_json::Value>(
|
||||
&vm.eval("JSON.stringify(defaultFacts)").unwrap()
|
||||
)
|
||||
.unwrap(),
|
||||
serde_json::json!(vec![vec![true; 7]; 3]),
|
||||
"default wrappers: {policy:?}"
|
||||
);
|
||||
for (expression, expected) in [
|
||||
(
|
||||
"JSON.stringify(facts)",
|
||||
serde_json::json!(vec![vec![true; 10]; 3]),
|
||||
),
|
||||
(
|
||||
"JSON.stringify(objectFacts)",
|
||||
serde_json::json!(vec![vec![true; 4]; 3]),
|
||||
),
|
||||
(
|
||||
"JSON.stringify(savedEvents.map(event => [event.currentTarget === null, event.composedPath().length === 0, event.defaultPrevented]))",
|
||||
serde_json::json!(vec![vec![true; 3]; 3]),
|
||||
),
|
||||
] {
|
||||
assert_eq!(
|
||||
serde_json::from_str::<serde_json::Value>(
|
||||
&vm.eval_in_isolated_context(isolated, expression).unwrap()
|
||||
)
|
||||
.unwrap(),
|
||||
expected,
|
||||
"{expression}: {policy:?}"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
vm.layout_pass_observability_for_test().1,
|
||||
builds,
|
||||
"Node event projection must not obtain layout: {policy:?}"
|
||||
);
|
||||
vm.destroy_isolated_world_context(isolated);
|
||||
assert_eq!(vm.eval(r#"
|
||||
const later = new CustomEvent('node-world-0', {cancelable: true});
|
||||
originals[0] = later;
|
||||
JSON.stringify([button.dispatchEvent(later), later.defaultPrevented, defaultFacts.length]);
|
||||
"#).unwrap(), "[true,false,4]", "retired Node listeners: {policy:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn bubbling_node_events_project_the_whole_path_and_share_stop_flags() {
|
||||
for policy in [
|
||||
moli_page_types::LayoutPolicy::Mock,
|
||||
moli_page_types::LayoutPolicy::OnDemand,
|
||||
] {
|
||||
let mut vm = new_storage_test_vm("https://bubbling-node-worlds.test/");
|
||||
vm.set_layout_policy(policy);
|
||||
vm.eval(
|
||||
r#"
|
||||
globalThis.container = document.createElement('div');
|
||||
container.id = 'container';
|
||||
globalThis.button = document.createElement('button');
|
||||
button.id = 'probe';
|
||||
container.appendChild(button);
|
||||
(document.body || document.documentElement || document).appendChild(container);
|
||||
globalThis.sideEffects = 0;
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let isolated = vm
|
||||
.create_isolated_world("bubbling-node-listeners", false)
|
||||
.unwrap();
|
||||
vm.eval_in_isolated_context(
|
||||
isolated,
|
||||
r#"
|
||||
globalThis.button = document.getElementById('probe');
|
||||
globalThis.container = document.getElementById('container');
|
||||
globalThis.facts = [];
|
||||
globalThis.firstEvent = null;
|
||||
function observe(label, target, phase) {
|
||||
target.addEventListener('bubble-world', function(event) {
|
||||
if (!firstEvent) firstEvent = event;
|
||||
const path = event.composedPath();
|
||||
facts.push([label, this === target, event === firstEvent,
|
||||
event instanceof Event, event.target === button,
|
||||
event.currentTarget === target, event.eventPhase === phase,
|
||||
path[0] === button, path.includes(container), path.includes(document),
|
||||
path[path.length - 1] === window, window.event === event]);
|
||||
event.preventDefault();
|
||||
}, phase === 1);
|
||||
}
|
||||
observe('capture', document, 1);
|
||||
observe('node', button, 2);
|
||||
observe('ancestor', container, 3);
|
||||
observe('window', window, 3);
|
||||
button.addEventListener('stop-world', event => {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
});
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
vm.eval(
|
||||
r#"
|
||||
button.addEventListener('stop-world', () => sideEffects++);
|
||||
document.addEventListener('stop-world', () => sideEffects++);
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let builds = vm.layout_pass_observability_for_test().1;
|
||||
assert_eq!(
|
||||
vm.eval(
|
||||
r#"
|
||||
const first = new Event('bubble-world', {bubbles: true, cancelable: true});
|
||||
const second = new Event('stop-world', {bubbles: true, cancelable: true});
|
||||
JSON.stringify([button.dispatchEvent(first), first.defaultPrevented,
|
||||
button.dispatchEvent(second), second.defaultPrevented, sideEffects]);
|
||||
"#
|
||||
)
|
||||
.unwrap(),
|
||||
"[false,true,false,true,0]",
|
||||
"cancellation and propagation across worlds: {policy:?}"
|
||||
);
|
||||
let result = vm
|
||||
.eval_in_isolated_context(isolated, "JSON.stringify(facts)")
|
||||
.unwrap();
|
||||
let expected: Vec<_> = ["capture", "node", "ancestor", "window"]
|
||||
.into_iter()
|
||||
.map(|label| {
|
||||
let mut row = vec![serde_json::json!(label)];
|
||||
row.extend(std::iter::repeat_n(serde_json::json!(true), 11));
|
||||
row
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
serde_json::from_str::<serde_json::Value>(&result).unwrap(),
|
||||
serde_json::json!(expected),
|
||||
"Node-to-Window path projection: {policy:?}"
|
||||
);
|
||||
assert_eq!(vm.eval_in_isolated_context(isolated, "JSON.stringify([firstEvent.currentTarget === null, firstEvent.composedPath().length === 0, firstEvent.defaultPrevented])").unwrap(), "[true,true,true]");
|
||||
assert_eq!(
|
||||
vm.layout_pass_observability_for_test().1,
|
||||
builds,
|
||||
"bubbling must not obtain layout: {policy:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn borrowed_default_world_node_callback_keeps_the_target_documents_wrappers() {
|
||||
for policy in [
|
||||
moli_page_types::LayoutPolicy::Mock,
|
||||
moli_page_types::LayoutPolicy::OnDemand,
|
||||
] {
|
||||
let mut vm = new_storage_test_vm("https://borrowed-node-world.test/");
|
||||
vm.set_layout_policy(policy);
|
||||
vm.eval(
|
||||
r#"
|
||||
globalThis.frame = document.createElement('iframe');
|
||||
globalThis.button = document.createElement('button');
|
||||
const container = document.createElement('div');
|
||||
(document.body || document.documentElement || document).appendChild(container);
|
||||
container.append(button, frame);
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
materialize_single_child_default_realm_for_test(&mut vm, "borrowed Node callback");
|
||||
vm.eval(r#"
|
||||
globalThis.facts = [];
|
||||
globalThis.original = null;
|
||||
button.addEventListener('borrowed-node', frame.contentWindow.Function('event', `
|
||||
parent.facts.push([
|
||||
this === parent.button, this instanceof parent.Node, !(this instanceof Node),
|
||||
event === parent.original, event instanceof parent.Event, !(event instanceof Event),
|
||||
event.target === parent.button, event.currentTarget === parent.button,
|
||||
event.composedPath()[0] === parent.button, window.event === event
|
||||
]);
|
||||
`));
|
||||
"#).unwrap();
|
||||
let builds = vm.layout_pass_observability_for_test().1;
|
||||
let result = vm
|
||||
.eval(
|
||||
r#"
|
||||
original = new Event('borrowed-node');
|
||||
button.dispatchEvent(original);
|
||||
JSON.stringify(facts);
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
serde_json::from_str::<serde_json::Value>(&result).unwrap(),
|
||||
serde_json::json!([vec![true; 10]]),
|
||||
"borrowed callback must retain the real target Window: {policy:?}"
|
||||
);
|
||||
assert_eq!(vm.layout_pass_observability_for_test().1, builds);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user