fix(events): preserve window.event during exception reporting

This commit is contained in:
ldm0
2026-09-22 21:43:34 +08:00
parent 3e52f0b0a3
commit 1e03ea5a60
15 changed files with 524 additions and 56 deletions
+78 -13
View File
@@ -192,6 +192,60 @@ impl CallbackInvoker {
callback_name: &str,
invocation: CallbackInvocation<'s, '_>,
) -> CallbackInvocationOutcome {
Self::invoke_with_completion(
scope,
callback_kind,
log_label,
log_level,
callback_name,
invocation,
false,
|_scope, outcome| outcome,
)
}
pub(crate) fn invoke_event_and_then<'s, R>(
scope: &mut v8::PinScope<'s, '_>,
callback_kind: &str,
log_label: &str,
log_level: CallbackExceptionLogLevel,
callback_name: &str,
invocation: CallbackInvocation<'s, '_>,
complete: impl FnOnce(&mut v8::PinScope<'s, '_>, CallbackInvocationOutcome) -> R,
) -> R {
let host_ptr = invocation.host_ptr;
let event_callback_scope = host_ptr
.map(|host_ptr| unsafe { &mut *host_ptr }.enter_event_callback_invocation_scope());
let defer_window_event_restore = event_callback_scope
.as_ref()
.is_some_and(|event_scope| event_scope.is_outermost())
&& host_ptr
.is_some_and(|host_ptr| !unsafe { &*host_ptr }.explicit_event_dispatch_is_active());
let result = Self::invoke_with_completion(
scope,
callback_kind,
log_label,
log_level,
callback_name,
invocation,
defer_window_event_restore,
complete,
);
drop(event_callback_scope);
result
}
#[allow(clippy::too_many_arguments)]
fn invoke_with_completion<'s, R>(
scope: &mut v8::PinScope<'s, '_>,
callback_kind: &str,
log_label: &str,
log_level: CallbackExceptionLogLevel,
callback_name: &str,
invocation: CallbackInvocation<'s, '_>,
defer_window_event_restore: bool,
complete: impl FnOnce(&mut v8::PinScope<'s, '_>, CallbackInvocationOutcome) -> R,
) -> R {
if let Some(host_ptr) = invocation.host_ptr {
unsafe { &*host_ptr }.debug_assert_not_in_structural_mutation("callback invocation");
}
@@ -199,10 +253,10 @@ impl CallbackInvoker {
(invocation.host_ptr, invocation.relevant_identity)
&& !unsafe { &*host_ptr }.window_execution_context_identity_is_current(identity)
{
return CallbackInvocationOutcome::Retired;
return complete(scope, CallbackInvocationOutcome::Retired);
}
let result = with_webidl_callback_contexts(
with_webidl_callback_contexts(
scope,
invocation.relevant_context,
invocation.incumbent_context,
@@ -255,18 +309,29 @@ impl CallbackInvoker {
},
);
if let Some(previous) = previous_window_event {
let global = relevant_context.global(scope);
let _ = global.set(scope, v8str(scope, WINDOW_EVENT_SLOT).into(), previous);
}
result
},
);
let outcome = match result {
Ok(value) => CallbackInvocationOutcome::Returned(value),
Err(report) => CallbackInvocationOutcome::Threw(report),
};
let completed = complete(scope, outcome);
match result {
Ok(value) => CallbackInvocationOutcome::Returned(value),
Err(report) => CallbackInvocationOutcome::Threw(report),
}
if let Some(previous) = previous_window_event {
if defer_window_event_restore && let Some(host_ptr) = invocation.host_ptr {
crate::context_bootstrap::enqueue_window_event_restore_after_microtask_checkpoint(
scope,
host_ptr,
invocation.relevant_identity,
relevant_context,
previous,
);
} else {
let global = relevant_context.global(scope);
let _ = global.set(scope, v8str(scope, WINDOW_EVENT_SLOT).into(), previous);
}
}
completed
},
)
}
}
@@ -328,6 +328,7 @@ pub(crate) use self::message_ports::{
};
use self::message_ports::{schedule_host_callback, schedule_scope_callback};
pub(crate) use self::microtask_checkpoint::{
enqueue_window_event_restore_after_microtask_checkpoint,
install_agent_microtask_checkpoint_tasks, run_end_of_microtask_checkpoint_tasks,
};
pub(crate) use self::navigation_bootstrap::{
@@ -1,4 +1,5 @@
use super::*;
use crate::util::context_host_ptr_from_global_bridge;
pub(crate) fn simple_event_target_add_event_listener_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
@@ -37,5 +38,7 @@ pub(crate) fn simple_event_target_dispatch_event_callback<'s>(
rv.set(v8::Boolean::new(scope, true).into());
return;
};
let _explicit_dispatch_scope = context_host_ptr_from_global_bridge(scope)
.map(|host_ptr| unsafe { &mut *host_ptr }.enter_explicit_event_dispatch_scope());
simple_object_event_target_dispatch(scope, &args, slot_name, &mut rv);
}
@@ -257,28 +257,29 @@ fn invoke_simple_event_callback_with_invocation<'s>(
let target_name = simple_event_target_interface_name(scope, callback_target);
host.schedule_dom_debugger_event_listener_pause_for_interface(event_type, &target_name)
});
match CallbackInvoker::invoke(
CallbackInvoker::invoke_event_and_then(
scope,
"event listener",
"simple event listener threw",
CallbackExceptionLogLevel::Debug,
callback_name,
invocation,
) {
CallbackInvocationOutcome::Returned(value) => Some(value),
CallbackInvocationOutcome::Threw(report) => {
if let Some(host_ptr) = host_ptr {
report_event_callback_exception(
scope,
host_ptr,
event_type,
relevant_identity,
None,
&report,
);
|scope, outcome| match outcome {
CallbackInvocationOutcome::Returned(value) => Some(value),
CallbackInvocationOutcome::Threw(report) => {
if let Some(host_ptr) = host_ptr {
report_event_callback_exception(
scope,
host_ptr,
event_type,
relevant_identity,
None,
&report,
);
}
None
}
None
}
CallbackInvocationOutcome::Retired => None,
}
CallbackInvocationOutcome::Retired => None,
},
)
}
@@ -10,6 +10,38 @@ enum AgentMicrotaskCheckpointTask {
context: v8::Global<v8::Context>,
transaction: v8::Global<v8::Object>,
},
RestoreWindowEvent {
host_ptr: *mut crate::native_bridge::JsContextHost,
relevant_identity: Option<crate::native_bridge::WindowExecutionContextIdentity>,
context: v8::Global<v8::Context>,
previous: v8::Global<v8::Value>,
},
}
pub(crate) fn enqueue_window_event_restore_after_microtask_checkpoint(
scope: &mut v8::PinScope<'_, '_>,
host_ptr: *mut crate::native_bridge::JsContextHost,
relevant_identity: Option<crate::native_bridge::WindowExecutionContextIdentity>,
context: v8::Local<'_, v8::Context>,
previous: v8::Local<'_, v8::Value>,
) {
if scope.get_slot::<AgentMicrotaskCheckpointTasks>().is_none() {
assert!(
scope.set_slot(AgentMicrotaskCheckpointTasks::default()),
"agent checkpoint state should be installed once on first use"
);
}
let task = AgentMicrotaskCheckpointTask::RestoreWindowEvent {
host_ptr,
relevant_identity,
context: v8::Global::new(scope, context),
previous: v8::Global::new(scope, previous),
};
scope
.get_slot_mut::<AgentMicrotaskCheckpointTasks>()
.expect("agent checkpoint state should exist after installation")
.tasks
.push(task);
}
pub(crate) fn install_agent_microtask_checkpoint_tasks(isolate: &mut v8::Isolate) {
@@ -50,14 +82,27 @@ pub(crate) fn run_end_of_microtask_checkpoint_tasks(scope: &mut v8::PinScope<'_,
return;
};
let mut window_event_restores = Vec::new();
for task in tasks {
match task {
AgentMicrotaskCheckpointTask::DeactivateIndexedDbTransaction {
context,
transaction,
} => run_indexed_db_transaction_deactivation(scope, context, transaction),
AgentMicrotaskCheckpointTask::RestoreWindowEvent {
host_ptr,
relevant_identity,
context,
previous,
} => {
window_event_restores.push((host_ptr, relevant_identity, context, previous));
}
}
}
for (host_ptr, relevant_identity, context, previous) in window_event_restores.into_iter().rev()
{
restore_window_event(scope, host_ptr, relevant_identity, context, previous);
}
}
fn run_indexed_db_transaction_deactivation(
@@ -73,3 +118,26 @@ fn run_indexed_db_transaction_deactivation(
transaction,
);
}
fn restore_window_event(
scope: &mut v8::PinScope<'_, '_>,
host_ptr: *mut crate::native_bridge::JsContextHost,
relevant_identity: Option<crate::native_bridge::WindowExecutionContextIdentity>,
context: v8::Global<v8::Context>,
previous: v8::Global<v8::Value>,
) {
if relevant_identity.is_some_and(|identity| {
!unsafe { &*host_ptr }.window_execution_context_identity_is_current(identity)
}) {
return;
}
let context = v8::Local::new(scope, &context);
let scope = &mut v8::ContextScope::new(scope, context);
let global = context.global(scope);
let previous = v8::Local::new(scope, &previous);
let _ = global.set(
scope,
crate::util::v8str(scope, crate::host::WINDOW_EVENT_SLOT).into(),
previous,
);
}
@@ -630,11 +630,11 @@ fn window_length_replaceable_getter<'s>(
fn window_event_replaceable_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
_args: v8::FunctionCallbackArguments<'s>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
rv.set(
global_hidden_value(scope, WINDOW_EVENT_SLOT)
window_event_value_for_receiver(scope, args.this())
.unwrap_or_else(|| v8::undefined(scope).into()),
);
}
@@ -158,9 +158,10 @@ pub(crate) const BODY_OR_FRAMESET_WINDOW_EVENT_HANDLER_PROPERTIES: &[&str] = &[
pub(super) use accessors::{
window_console_getter, window_event_getter, window_event_setter,
window_onerror_getter_function, window_onerror_setter_function,
window_onrejectionhandled_getter_function, window_onrejectionhandled_setter_function,
window_onunhandledrejection_getter_function, window_onunhandledrejection_setter_function,
window_event_value_for_receiver, window_onerror_getter_function,
window_onerror_setter_function, window_onrejectionhandled_getter_function,
window_onrejectionhandled_setter_function, window_onunhandledrejection_getter_function,
window_onunhandledrejection_setter_function,
};
pub(super) use console::{
console_assert_callback, console_debug_callback, console_error_callback,
@@ -142,12 +142,37 @@ pub(in crate::context_bootstrap) fn window_event_getter<'s>(
if !require_window_receiver(scope, &args) {
return;
}
match global_hidden_value(scope, WINDOW_EVENT_SLOT) {
match window_event_value_for_receiver(scope, args.this()) {
Some(value) => rv.set(value),
None => rv.set(v8::undefined(scope).into()),
}
}
pub(in crate::context_bootstrap) fn window_event_value_for_receiver<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Value>> {
let target_event = context_host_ptr_from_window_object(scope, receiver)
.or_else(|| context_host_ptr_from_global_bridge(scope))
.and_then(|host_ptr| {
let dispatch_scope = if let Some(popup_id) =
crate::native_bridge::lightweight_popup_id_from_window(scope, receiver)
{
crate::native_bridge::OwnerDispatchScope::LightweightPopup(popup_id)
} else if let Some(handle) = window_child_context_handle(scope, receiver) {
crate::native_bridge::OwnerDispatchScope::Child(handle)
} else {
crate::native_bridge::OwnerDispatchScope::Top
};
let host = unsafe { &*host_ptr };
let owner = host.current_window_execution_context_owner(dispatch_scope)?;
let (_, context) = host.window_execution_context(scope, owner, dispatch_scope)?;
let global = context.global(scope);
object_own_hidden_value(scope, global, WINDOW_EVENT_SLOT)
});
target_event.or_else(|| global_hidden_value(scope, WINDOW_EVENT_SLOT))
}
pub(in crate::context_bootstrap) fn window_event_setter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
@@ -132,6 +132,21 @@ pub(crate) fn dispatch_window_error_event_with_details<'s>(
colno: u32,
error_value: Option<v8::Local<'s, v8::Value>>,
) -> std::result::Result<(), String> {
let current_context = scope.get_current_context();
let reporting_owner = unsafe { &*host_ptr }
.window_execution_context_identity_for_v8_context(scope, current_context)
.map(|identity| identity.owner());
let _error_reporting_scope = if let Some(owner) = reporting_owner {
let Some(reporting_scope) =
(unsafe { &mut *host_ptr }).enter_window_error_reporting_scope(owner)
else {
return Ok(());
};
Some(reporting_scope)
} else {
None
};
let global = scope.get_current_context().global(scope);
ensure_window_reflecting_body_onerror_handler(scope);
let error_value = error_value.unwrap_or_else(|| v8::null(scope).into());
+36 -20
View File
@@ -302,28 +302,29 @@ fn invoke_prepared_event_callback_with_receiver<'s>(
current_event,
)
.with_execution_context_currentness(host_ptr, relevant_identity);
match CallbackInvoker::invoke(
CallbackInvoker::invoke_event_and_then(
scope,
"event listener",
"host event listener threw",
crate::exception_reporting::CallbackExceptionLogLevel::Debug,
callback_name,
invocation,
) {
CallbackInvocationOutcome::Returned(value) => Some(value),
CallbackInvocationOutcome::Threw(report) => {
report_event_callback_exception(
scope,
host_ptr,
event_type,
relevant_identity,
None,
&report,
);
None
}
CallbackInvocationOutcome::Retired => None,
}
|scope, outcome| match outcome {
CallbackInvocationOutcome::Returned(value) => Some(value),
CallbackInvocationOutcome::Threw(report) => {
report_event_callback_exception(
scope,
host_ptr,
event_type,
relevant_identity,
None,
&report,
);
None
}
CallbackInvocationOutcome::Retired => None,
},
)
}
fn invoke_event_handler_property<'s>(
@@ -573,10 +574,6 @@ pub(crate) fn report_event_callback_exception<'s>(
child_handle: Option<DomHandle>,
report: &V8ExceptionReport,
) {
if event_type == "error" {
return;
}
let child_handle = child_handle.or_else(|| {
relevant_identity.and_then(|identity| match identity.dispatch_scope() {
crate::native_bridge::OwnerDispatchScope::Child(handle) => Some(handle),
@@ -585,6 +582,25 @@ pub(crate) fn report_event_callback_exception<'s>(
})
});
if let Some(handle) = child_handle {
let reporting_owner = relevant_identity
.map(|identity| identity.owner())
.or_else(|| {
unsafe { &*host_ptr }.current_window_execution_context_owner(
crate::native_bridge::OwnerDispatchScope::Child(handle),
)
});
let _error_reporting_scope = if let Some(owner) = reporting_owner {
let Some(reporting_scope) =
(unsafe { &mut *host_ptr }).enter_window_error_reporting_scope(owner)
else {
return;
};
Some(reporting_scope)
} else if event_type == "error" {
return;
} else {
None
};
let event = {
let host = unsafe { &mut *host_ptr };
host.child_browsing_context_window_wrapper(scope, handle)
@@ -370,6 +370,9 @@ impl JsContextHost {
child_window_event_listeners: HashMap::new(),
next_child_window_event_registration_id: 0,
event_callbacks: Default::default(),
explicit_event_dispatch_depth: 0,
event_callback_invocation_depth: 0,
active_window_error_report_owners: HashSet::new(),
browser_context_runtime,
top_level_navigation_handoff_tx,
service_worker_task_tx,
@@ -17,6 +17,55 @@ pub(crate) struct PreparedEventCallback {
relevant_identity: Option<WindowExecutionContextIdentity>,
}
pub(crate) struct WindowErrorReportingScope {
host_ptr: *mut JsContextHost,
owner: WindowExecutionContextOwner,
}
pub(crate) struct ExplicitEventDispatchScope {
host_ptr: *mut JsContextHost,
}
pub(crate) struct EventCallbackInvocationScope {
host_ptr: *mut JsContextHost,
outermost: bool,
}
impl EventCallbackInvocationScope {
pub(crate) fn is_outermost(&self) -> bool {
self.outermost
}
}
impl Drop for EventCallbackInvocationScope {
fn drop(&mut self) {
let host = unsafe { &mut *self.host_ptr };
host.event_callback_invocation_depth = host
.event_callback_invocation_depth
.checked_sub(1)
.expect("event callback invocation scope must be active");
}
}
impl Drop for ExplicitEventDispatchScope {
fn drop(&mut self) {
let host = unsafe { &mut *self.host_ptr };
host.explicit_event_dispatch_depth = host
.explicit_event_dispatch_depth
.checked_sub(1)
.expect("explicit EventTarget dispatch scope must be active");
}
}
impl Drop for WindowErrorReportingScope {
fn drop(&mut self) {
let removed = unsafe { &mut *self.host_ptr }
.active_window_error_report_owners
.remove(&self.owner);
debug_assert!(removed, "Window error reporting scope must be active");
}
}
impl PreparedEventCallback {
pub(crate) fn callback<'s>(
&self,
@@ -80,6 +129,36 @@ impl EventCallbackRegistry {
}
impl JsContextHost {
pub(crate) fn enter_event_callback_invocation_scope(&mut self) -> EventCallbackInvocationScope {
let outermost = self.event_callback_invocation_depth == 0;
self.event_callback_invocation_depth += 1;
EventCallbackInvocationScope {
host_ptr: self,
outermost,
}
}
pub(crate) fn enter_explicit_event_dispatch_scope(&mut self) -> ExplicitEventDispatchScope {
self.explicit_event_dispatch_depth += 1;
ExplicitEventDispatchScope { host_ptr: self }
}
pub(crate) fn explicit_event_dispatch_is_active(&self) -> bool {
self.explicit_event_dispatch_depth != 0
}
pub(crate) fn enter_window_error_reporting_scope(
&mut self,
owner: WindowExecutionContextOwner,
) -> Option<WindowErrorReportingScope> {
self.active_window_error_report_owners
.insert(owner)
.then(|| WindowErrorReportingScope {
host_ptr: self,
owner,
})
}
pub(crate) fn register_target_event_listener<'s>(
&mut self,
scope: &mut v8::PinScope<'s, '_>,
@@ -982,6 +982,9 @@ pub(crate) struct JsContextHost {
HashMap<DomHandle, IndexMap<String, Vec<ChildWindowEventListenerEntry>>>,
next_child_window_event_registration_id: u64,
event_callbacks: event_callbacks::EventCallbackRegistry,
explicit_event_dispatch_depth: usize,
event_callback_invocation_depth: usize,
active_window_error_report_owners: HashSet<WindowExecutionContextOwner>,
browser_context_runtime: crate::runtime::RendererBrowserContextRuntime,
top_level_navigation_handoff_tx:
crate::page_task_queue::RendererTopLevelNavigationHandoffSender,
@@ -2175,6 +2175,192 @@ fn cross_realm_listener_throw_reports_listener_global_not_target_global() {
r#"[["function",true,true],["function-window-event-restored",true],["object",true,true],["object-window-event-restored",true]]"#
);
}
#[test]
fn listener_exception_reporting_preserves_outer_realm_window_event() {
let mut vm = new_storage_test_vm("https://event-listener-nested-window-event.test/");
vm.eval(
r#"
(() => {
const host = document.body || document.documentElement || document;
const listenerFrame = document.createElement("iframe");
listenerFrame.srcdoc = `<script>
function listener() { throw new Error("listener failure"); }
function currentEventType() { return window.event?.type; }
<\/script>`;
const handlerFrame = document.createElement("iframe");
host.appendChild(listenerFrame);
host.appendChild(handlerFrame);
globalThis.__nestedEventFrames = [listenerFrame, handlerFrame];
return "ready";
})()
"#,
)
.expect("nested window.event frame setup should evaluate");
vm.drain_pending_child_frame_work_for_test();
let result = vm
.eval(
r#"
(() => {
const [listenerFrame, handlerFrame] = globalThis.__nestedEventFrames;
const listenerWindow = listenerFrame.contentWindow;
const handlerWindow = handlerFrame.contentWindow;
listenerWindow.onerror = new handlerWindow.Function(`
parent.__nestedWindowEventState = [
parent.__nestedEventFrames[0].contentWindow.currentEventType(),
window.event?.type
];
return true;
`);
const target = new EventTarget();
target.addEventListener("party", listenerWindow.listener);
target.dispatchEvent(new Event("party"));
return JSON.stringify({
during: globalThis.__nestedWindowEventState,
listenerAfter: listenerWindow.event,
handlerAfter: handlerWindow.event
});
})()
"#,
)
.expect("nested window.event exception reporting should evaluate");
assert_eq!(result, r#"{"during":["party","error"]}"#,);
}
#[test]
fn cross_realm_window_event_getters_preserve_nested_handler_events() {
let mut vm = new_storage_test_vm("https://nested-handler-window-event.test/");
vm.eval(
r#"
(() => {
const host = document.body || document.documentElement || document;
const firstFrame = document.createElement("iframe");
const secondFrame = document.createElement("iframe");
host.appendChild(firstFrame);
host.appendChild(secondFrame);
globalThis.__handlerEventFrames = [firstFrame, secondFrame];
return "ready";
})()
"#,
)
.expect("nested handler frame setup should evaluate");
vm.drain_pending_child_frame_work_for_test();
let result = vm
.eval(
r#"
(() => {
const [firstFrame, secondFrame] = globalThis.__handlerEventFrames;
firstFrame.contentWindow.onerror = new secondFrame.contentWindow.Function(`
top.__secondHandlerEvents = [
top.event?.type,
top.frames[0].event?.type,
top.frames[1].event?.type,
top.frames[1].event?.error?.name
];
return true;
`);
window.onerror = new firstFrame.contentWindow.Function(`
top.__firstHandlerEvents = [
top.event?.type,
top.frames[0].event?.type,
top.frames[1].event?.type ?? null
];
missingNestedHandlerName;
`);
window.onload = loadEvent => {
const errorEvent = new ErrorEvent("error", {
error: new Error("outer error")
});
window.dispatchEvent(errorEvent);
};
window.dispatchEvent(new Event("load"));
return JSON.stringify({
first: globalThis.__firstHandlerEvents,
second: globalThis.__secondHandlerEvents,
after: [window.event, frames[0].event, frames[1].event]
});
})()
"#,
)
.expect("nested cross-realm Window event handlers should evaluate");
assert_eq!(
result,
r#"{"first":["load","error",null],"second":["load","error","error","ReferenceError"],"after":[null,null,null]}"#,
);
}
#[test]
fn same_realm_onerror_throw_does_not_reenter_error_reporting() {
let mut vm = new_storage_test_vm("https://onerror-recursion.test/");
let result = vm
.eval(
r#"
(() => {
let calls = 0;
window.onerror = () => {
calls++;
throw new Error("nested onerror failure");
};
window.reportError(new Error("initial failure"));
return String(calls);
})()
"#,
)
.expect("same-realm onerror recursion probe should evaluate");
assert_eq!(result, "1");
}
#[test]
fn host_event_microtasks_observe_current_window_event_until_checkpoint_cleanup() {
let mut vm = new_storage_test_vm("https://host-event-microtasks.test/");
vm.eval(
r#"
(() => {
const frame = document.createElement("iframe");
frame.srcdoc = `<script>window.__finalSrcdocRealm = true;<\/script>`;
globalThis.__hostEventMicrotaskState = null;
frame.addEventListener("load", event => {
if (!frame.contentWindow.__finalSrcdocRealm) return;
Promise.resolve().then(() => {
globalThis.__hostEventMicrotaskState = [
window.event === event,
window.event?.type
];
});
});
(document.body || document.documentElement || document).appendChild(frame);
return "ready";
})()
"#,
)
.expect("host-entered event microtask setup should evaluate");
vm.drain_pending_child_frame_work_for_test();
vm.perform_owner_lane_task_microtask_checkpoints()
.expect("host event task checkpoint should complete");
let result = vm
.eval(
r#"JSON.stringify({
during: globalThis.__hostEventMicrotaskState,
after: window.event ?? null
})"#,
)
.expect("host-entered event microtask state should evaluate");
assert_eq!(result, r#"{"during":[true,"load"],"after":null}"#);
}
#[test]
fn event_timestamp_uses_performance_origin_and_safe_resolution() {
let mut vm = new_storage_test_vm("https://event-timestamp.test/");
+2
View File
@@ -497,6 +497,8 @@ pub(super) fn event_target_dispatch_event_callback<'s>(
return;
}
let _explicit_dispatch_scope = host.enter_explicit_event_dispatch_scope();
let event_type = event_type_string(scope, event);
if let Some(handle) = child_window_target {
let event_type = event_type.as_deref().unwrap_or_default();