From 9283dfd2dc3f53781db42668438c4bc9d2f4e470 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sun, 30 Aug 2026 22:20:04 +0800 Subject: [PATCH] fix(events): implement beforeunload return semantics --- moli-renderer-v8/src/context_bootstrap.rs | 2 +- .../src/context_bootstrap/event_document.rs | 14 ++- .../src/context_bootstrap/event_template.rs | 39 ++++-- .../src/context_bootstrap/events.rs | 102 ++++++++++++++- .../src/context_bootstrap/events/methods.rs | 14 +++ .../context_bootstrap/navigation_events.rs | 7 +- moli-renderer-v8/src/host.rs | 3 +- moli-renderer-v8/src/host/events.rs | 119 +++++++++++++++++- .../context_host/child_events.rs | 17 ++- .../event_dispatch_and_targets.rs | 71 +++++++++++ .../browser_api/navigation/navigate_event.rs | 53 ++++++++ 11 files changed, 420 insertions(+), 21 deletions(-) diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index 4ed0c60d53..f30b8943f4 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -259,7 +259,7 @@ pub(crate) use self::dom_rect::build_dom_rect_object; pub(crate) use self::dom_rect_list::{build_dom_rect_list_object}; pub(crate) use self::events::{ EVENT_DISPATCHING_SLOT, EVENT_PASSIVE_SLOT, EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT, - EVENT_STOP_PROPAGATION_SLOT, EventHandlerType, apply_event_handler_return_value, clear_event_composed_path, event_attribute, event_backing, + EVENT_STOP_PROPAGATION_SLOT, EventHandlerType, apply_before_unload_event_handler_return_value, apply_event_handler_return_value, clear_event_composed_path, 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_event_trusted, new_event_state, new_event_wrapper, diff --git a/moli-renderer-v8/src/context_bootstrap/event_document.rs b/moli-renderer-v8/src/context_bootstrap/event_document.rs index 8dd88031c2..29145418b6 100644 --- a/moli-renderer-v8/src/context_bootstrap/event_document.rs +++ b/moli-renderer-v8/src/context_bootstrap/event_document.rs @@ -1,4 +1,6 @@ -use super::events::{new_uninitialized_text_event, set_event_initialized}; +use super::events::{ + new_uninitialized_before_unload_event, new_uninitialized_text_event, set_event_initialized, +}; use super::*; use crate::webidl; use std::str::FromStr; @@ -42,7 +44,7 @@ enum DocumentCreateEventKind { impl DocumentCreateEventKind { fn constructor_name(self) -> &'static str { match self { - DocumentCreateEventKind::BeforeUnloadEvent => "Event", + DocumentCreateEventKind::BeforeUnloadEvent => "BeforeUnloadEvent", DocumentCreateEventKind::Event => "Event", DocumentCreateEventKind::CustomEvent => "CustomEvent", DocumentCreateEventKind::DeviceMotionEvent => "DeviceMotionEvent", @@ -73,8 +75,12 @@ fn new_uninitialized_document_event<'s>( let name = kind.constructor_name(); let constructor = super::exposed_interfaces::ensure_intrinsic_interface_constructor(scope, name).ok()?; - if kind == DocumentCreateEventKind::TextEvent { - return new_uninitialized_text_event(scope); + match kind { + DocumentCreateEventKind::BeforeUnloadEvent => { + return new_uninitialized_before_unload_event(scope); + } + DocumentCreateEventKind::TextEvent => return new_uninitialized_text_event(scope), + _ => {} } let empty_type = v8str(scope, ""); let event = constructor.new_instance(scope, &[empty_type.into()])?; diff --git a/moli-renderer-v8/src/context_bootstrap/event_template.rs b/moli-renderer-v8/src/context_bootstrap/event_template.rs index 5557a7c127..1e1cbb62b6 100644 --- a/moli-renderer-v8/src/context_bootstrap/event_template.rs +++ b/moli-renderer-v8/src/context_bootstrap/event_template.rs @@ -7,16 +7,18 @@ use super::{ ui_event_init_callback, }, events::{ - close_event_code_getter_function, close_event_reason_getter_function, - close_event_was_clean_getter_function, event_bubbles_getter_function, - event_cancel_bubble_getter_function, event_cancel_bubble_setter_function, - event_cancelable_getter_function, event_composed_getter_function, - event_composed_path_callback, event_current_target_getter_function, - event_default_prevented_getter_function, event_event_phase_getter_function, - event_prevent_default_callback, event_return_value_getter_function, - event_return_value_setter_function, event_src_element_getter_function, - event_stop_immediate_propagation_callback, event_stop_propagation_callback, - event_target_getter_function, event_time_stamp_getter_function, event_type_getter_function, + before_unload_event_return_value_getter_function, + before_unload_event_return_value_setter_function, close_event_code_getter_function, + close_event_reason_getter_function, close_event_was_clean_getter_function, + event_bubbles_getter_function, event_cancel_bubble_getter_function, + event_cancel_bubble_setter_function, event_cancelable_getter_function, + event_composed_getter_function, event_composed_path_callback, + event_current_target_getter_function, event_default_prevented_getter_function, + event_event_phase_getter_function, event_prevent_default_callback, + event_return_value_getter_function, event_return_value_setter_function, + event_src_element_getter_function, event_stop_immediate_propagation_callback, + event_stop_propagation_callback, event_target_getter_function, + event_time_stamp_getter_function, event_type_getter_function, focus_event_related_target_getter_function, form_data_event_form_data_getter_function, mouse_event_related_target_getter_function, pointer_event_get_predicted_events_callback, submit_event_submitter_getter_function, track_event_track_getter_function, @@ -130,6 +132,17 @@ struct EventBaseTemplateMethodsDeclaration { init_event: (), } +#[derive(WebApiFunctionTemplate)] +#[webapi(interface = web_api_interfaces::BeforeUnloadEvent, enumerable)] +struct BeforeUnloadEventTemplateAccessorsDeclaration { + #[webapi( + accessor_property = "returnValue", + getter = before_unload_event_return_value_getter_function, + setter = before_unload_event_return_value_setter_function + )] + return_value: (), +} + #[derive(WebApiFunctionTemplate)] #[webapi(interface = web_api_interfaces::CloseEvent, enumerable, receiver)] struct CloseEventTemplateAccessorsDeclaration { @@ -339,6 +352,12 @@ pub(super) fn install_event_template_bindings<'s>( super::events::install_device_event_template_bindings(scope, template, spec.interface.name()); match spec.interface.name() { + "BeforeUnloadEvent" => { + let proto = template.prototype_template(scope); + BeforeUnloadEventTemplateAccessorsDeclaration::initialize_prototype_template( + scope, proto, + ); + } "UIEvent" => { let proto = template.prototype_template(scope); UiEventTemplateMethodsDeclaration::initialize_prototype_template(scope, proto); diff --git a/moli-renderer-v8/src/context_bootstrap/events.rs b/moli-renderer-v8/src/context_bootstrap/events.rs index ad33143b78..de9b1a8dd6 100644 --- a/moli-renderer-v8/src/context_bootstrap/events.rs +++ b/moli-renderer-v8/src/context_bootstrap/events.rs @@ -29,6 +29,7 @@ const SUBMIT_EVENT_SUBMITTER_SLOT: &str = "__moliSubmitEventSubmitter"; const FORM_DATA_EVENT_FORM_DATA_SLOT: &str = "__moliFormDataEventFormData"; const TRACK_EVENT_TRACK_SLOT: &str = "__moliTrackEventTrack"; const EVENT_SUBCLASS_KIND_SLOT: &str = "__moliEventSubclassKind"; +const BEFORE_UNLOAD_EVENT_RETURN_VALUE_SLOT: &str = "__moliBeforeUnloadEventReturnValue"; #[derive(WebApiObject)] #[webapi(plain)] struct PageTransitionEventInitDeclaration { @@ -82,6 +83,102 @@ pub(crate) fn construct_original_event<'s>( Some(event) } +fn new_before_unload_event<'s>( + scope: &mut v8::PinScope<'s, '_>, + event_type: &str, + cancelable: bool, +) -> Option> { + let prototype = global_constructor_prototype(scope, "BeforeUnloadEvent")?; + let event = v8::Object::new(scope); + if event.set_prototype(scope, prototype.into()) != Some(true) { + return None; + } + base::initialize_event_object(scope, event, event_type, false, cancelable); + set_private_value( + scope, + event, + BEFORE_UNLOAD_EVENT_RETURN_VALUE_SLOT, + v8str(scope, "").into(), + ); + Some(event) +} + +pub(crate) fn construct_original_before_unload_event<'s>( + scope: &mut v8::PinScope<'s, '_>, +) -> Option> { + new_before_unload_event(scope, "beforeunload", true) +} + +pub(in crate::context_bootstrap) fn new_uninitialized_before_unload_event<'s>( + scope: &mut v8::PinScope<'s, '_>, +) -> Option> { + let event = new_before_unload_event(scope, "", false)?; + base::set_event_initialized(scope, event, false); + Some(event) +} + +fn before_unload_event_return_value<'s>( + scope: &mut v8::PinScope<'s, '_>, + event: v8::Local<'s, v8::Object>, +) -> Option> { + get_private_value(scope, event, BEFORE_UNLOAD_EVENT_RETURN_VALUE_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) +} + +pub(crate) fn event_is_before_unload_event<'s>( + scope: &mut v8::PinScope<'s, '_>, + event: v8::Local<'s, v8::Object>, +) -> bool { + before_unload_event_return_value(scope, event).is_some() +} + +pub(crate) fn set_before_unload_event_return_value<'s>( + scope: &mut v8::PinScope<'s, '_>, + event: v8::Local<'s, v8::Object>, + value: v8::Local<'s, v8::String>, +) { + set_private_value( + scope, + event, + BEFORE_UNLOAD_EVENT_RETURN_VALUE_SLOT, + value.into(), + ); +} + +pub(crate) fn before_unload_event_return_value_is_empty<'s>( + scope: &mut v8::PinScope<'s, '_>, + event: v8::Local<'s, v8::Object>, +) -> bool { + before_unload_event_return_value(scope, event).is_some_and(|value| value.length() == 0) +} + +pub(super) fn before_unload_event_return_value_getter_function<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(value) = before_unload_event_return_value(scope, args.this()) else { + throw_type_error(scope, "Illegal invocation"); + return; + }; + rv.set(value.into()); +} + +pub(super) fn before_unload_event_return_value_setter_function<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + _rv: v8::ReturnValue<'_, v8::Value>, +) { + if !event_is_before_unload_event(scope, args.this()) { + throw_type_error(scope, "Illegal invocation"); + return; + } + let Some(value) = args.get(0).to_string(scope) else { + return; + }; + set_before_unload_event_return_value(scope, args.this(), value); +} + pub(in crate::context_bootstrap) fn new_uninitialized_text_event<'s>( scope: &mut v8::PinScope<'s, '_>, ) -> Option> { @@ -367,7 +464,10 @@ pub(super) use base::{ }; pub(in crate::context_bootstrap) use init::parse_event_init; pub(super) use kind::EventSubclassKind; -pub(crate) use methods::{EventHandlerType, apply_event_handler_return_value}; +pub(crate) use methods::{ + EventHandlerType, apply_before_unload_event_handler_return_value, + apply_event_handler_return_value, +}; pub(super) use methods::{ event_cancel_bubble_getter_function, event_cancel_bubble_setter_function, event_composed_path_callback, event_prevent_default_callback, diff --git a/moli-renderer-v8/src/context_bootstrap/events/methods.rs b/moli-renderer-v8/src/context_bootstrap/events/methods.rs index aa9c173195..1faaa00687 100644 --- a/moli-renderer-v8/src/context_bootstrap/events/methods.rs +++ b/moli-renderer-v8/src/context_bootstrap/events/methods.rs @@ -43,6 +43,20 @@ pub(crate) fn apply_event_handler_return_value<'s>( } } +pub(crate) fn apply_before_unload_event_handler_return_value<'s>( + scope: &mut v8::PinScope<'s, '_>, + event: v8::Local<'s, v8::Object>, + returned: v8::Local<'s, v8::String>, +) { + if !event_is_before_unload_event(scope, event) { + return; + } + set_event_default_prevented(scope, event); + if before_unload_event_return_value_is_empty(scope, event) { + set_before_unload_event_return_value(scope, event, returned); + } +} + pub(in crate::context_bootstrap) fn event_prevent_default_callback<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_events.rs b/moli-renderer-v8/src/context_bootstrap/navigation_events.rs index 35cb2703a2..d75b8cb9c5 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_events.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_events.rs @@ -1,4 +1,7 @@ -use super::events::{construct_original_event, run_navigate_event_precommit_handlers}; +use super::events::{ + construct_original_before_unload_event, construct_original_event, + run_navigate_event_precommit_handlers, +}; use super::location_history_storage::{ NAVIGATION_ENTRY_EVENT_LISTENERS_SLOT, NAVIGATION_EVENT_LISTENERS_SLOT, }; @@ -630,7 +633,7 @@ pub(crate) fn dispatch_beforeunload_for_runtime_owner<'s>( scope: &mut v8::PinScope<'s, '_>, owner: v8::Local<'s, v8::Object>, ) { - let Some(event) = construct_original_event(scope, "beforeunload") else { + let Some(event) = construct_original_before_unload_event(scope) else { return; }; dispatch_unload_lifecycle_event_for_runtime_owner(scope, owner, "beforeunload", event); diff --git a/moli-renderer-v8/src/host.rs b/moli-renderer-v8/src/host.rs index fe01728cc6..77f9e40bfe 100644 --- a/moli-renderer-v8/src/host.rs +++ b/moli-renderer-v8/src/host.rs @@ -32,7 +32,8 @@ pub(super) use self::events::{ DispatchStatus, HostEventTargetRegistry, PublicEventDispatchResult, create_host_event, dispatch_host_event, dispatch_public_event, dispatch_public_event_with_original_target, event_dispatch_status, event_handler_content_attribute_owner, event_target_value, - host_event_defaults, invoke_prepared_event_callback, + host_event_defaults, invoke_prepared_before_unload_event_handler, + invoke_prepared_event_callback, report_event_callback_exception, report_event_listener_exception, }; pub(crate) use self::events::{EventListenerInspectorSnapshot, EventListenerRegistration}; diff --git a/moli-renderer-v8/src/host/events.rs b/moli-renderer-v8/src/host/events.rs index f13c798db0..68496c7c0c 100644 --- a/moli-renderer-v8/src/host/events.rs +++ b/moli-renderer-v8/src/host/events.rs @@ -1,7 +1,8 @@ use super::*; use crate::{ context_bootstrap::{ - CHILD_BROWSING_CONTEXT_HANDLE_SLOT, EventHandlerType, apply_event_handler_return_value, + CHILD_BROWSING_CONTEXT_HANDLE_SLOT, EventHandlerType, + apply_before_unload_event_handler_return_value, apply_event_handler_return_value, clear_event_composed_path, event_is_error_event, mark_event_trusted, set_event_composed_path, }, @@ -298,6 +299,98 @@ fn invoke_prepared_event_callback_with_receiver<'s>( ) } +#[allow(clippy::too_many_arguments)] +pub(crate) fn invoke_prepared_before_unload_event_handler<'s>( + scope: &mut v8::PinScope<'s, '_>, + host_ptr: *mut JsContextHost, + target: EventTargetHandle, + invocation_target_in_shadow_tree: bool, + event_type: &str, + callback_name: &str, + callback: crate::native_bridge::PreparedEventCallback, + event: v8::Local<'s, v8::Object>, +) { + let receiver = event_target_receiver(scope, host_ptr, target, event); + let _dom_debugger_pause = unsafe { &*host_ptr } + .schedule_dom_debugger_event_listener_pause_for_target(event_type, target); + let relevant_identity = callback.relevant_identity(); + let arguments = [event.into()]; + let invocation = CallbackInvocation::new( + callback.callback(scope), + receiver, + callback.relevant_context(scope), + callback.incumbent_context(scope), + callback.is_callable(), + "handleEvent", + &arguments, + (!invocation_target_in_shadow_tree).then_some(event), + ) + .with_execution_context_currentness(host_ptr, relevant_identity); + CallbackInvoker::invoke_event_and_then( + scope, + "event listener", + "host event listener threw", + crate::exception_reporting::CallbackExceptionLogLevel::Debug, + callback_name, + invocation, + |scope, outcome| match outcome { + CallbackInvocationOutcome::Returned(value) => { + let value = v8::Local::new(scope, value); + if value.is_null_or_undefined() { + return; + } + let conversion_report = { + let try_catch = std::pin::pin!(v8::TryCatch::new(scope)); + let mut conversion_scope = try_catch.init(); + match value.to_string(&conversion_scope) { + Some(value) => { + apply_before_unload_event_handler_return_value( + &mut conversion_scope, + event, + value, + ); + None + } + None if conversion_scope.has_caught() => { + let exception = conversion_scope.exception(); + let message = conversion_scope.message(); + let stack_trace = conversion_scope.stack_trace(); + Some(build_event_handler_exception_report( + &mut conversion_scope, + exception, + message, + stack_trace, + )) + } + None => None, + } + }; + if let Some(report) = conversion_report { + report_event_callback_exception( + scope, + host_ptr, + event_type, + relevant_identity, + None, + &report, + ); + } + } + CallbackInvocationOutcome::Threw(report) => { + report_event_callback_exception( + scope, + host_ptr, + event_type, + relevant_identity, + None, + &report, + ); + } + CallbackInvocationOutcome::Retired => {} + }, + ); +} + fn invoke_event_handler_property<'s>( registry: &mut HostEventTargetRegistry, scope: &mut v8::PinScope<'s, '_>, @@ -445,6 +538,30 @@ fn invoke_registered_event_handler<'s>( return; } + if event_type == "beforeunload" { + invoke_prepared_before_unload_event_handler( + scope, + host_ptr, + target, + invocation_target_in_shadow_tree, + event_type, + &handler_name, + prepared, + event, + ); + if let Some(timing_started) = timing_started { + tracing::info!( + target: "moli_cdp_nav_timing", + stage = "event_handler_property_invoked", + event_type, + handler_name, + ?target, + elapsed_ms = timing_started.elapsed().as_millis(), + ); + } + return; + } + let returned = invoke_prepared_event_callback( scope, host_ptr, diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_events.rs b/moli-renderer-v8/src/native_bridge/context_host/child_events.rs index b3965b6311..b83a90a7bc 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_events.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_events.rs @@ -8,7 +8,7 @@ use crate::{ frame_owner_model::{FrameDocumentTaskOwner, LocalWindowId}, host::{ ChildWindowEventTarget, DispatchStatus, create_host_event, event_dispatch_status, - invoke_prepared_event_callback, + invoke_prepared_before_unload_event_handler, invoke_prepared_event_callback, }, native_bridge::{ ACTIVE_CHILD_WINDOW_HANDLE_SLOT, EventCallbackId, PreparedEventCallback, @@ -626,6 +626,21 @@ impl JsContextHost { if !self.child_window_event_target_is_current(ready.target) { return (false, None); } + if ready.registration_kind == ChildWindowEventRegistrationKind::EventHandlerProperty + && event_type == "beforeunload" + { + invoke_prepared_before_unload_event_handler( + scope, + self as *mut JsContextHost, + EventTargetHandle::ChildWindow(ready.target), + false, + event_type, + &format!("child window {event_type} listener"), + ready.callback, + event, + ); + return (true, None); + } let arguments = child_window_event_callback_arguments( scope, ready.registration_kind, diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage/event_dispatch_and_targets.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage/event_dispatch_and_targets.rs index bc8f6de378..6b390009fa 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage/event_dispatch_and_targets.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage/event_dispatch_and_targets.rs @@ -1676,3 +1676,74 @@ fn selection_endpoints_follow_associated_live_range_mutations() { "" ); } + +#[test] +fn before_unload_event_uses_its_string_return_value_interface() { + let mut vm = new_storage_test_vm("https://before-unload-event-interface.test/"); + + let result = vm + .eval( + r#" + (() => { + const errorName = callback => { + try { + callback(); + return "none"; + } catch (error) { + return error.name; + } + }; + const descriptor = Object.getOwnPropertyDescriptor( + BeforeUnloadEvent.prototype, + "returnValue" + ); + const event = document.createEvent("BeforeUnloadEvent"); + const initial = [ + event instanceof BeforeUnloadEvent, + event instanceof Event, + Object.hasOwn(event, "returnValue"), + event.returnValue + ]; + event.initEvent("beforeunload", false, true); + event.returnValue = null; + const nullValue = event.returnValue; + event.returnValue = undefined; + const undefinedValue = event.returnValue; + event.returnValue = { toString() { return "object value"; } }; + const objectValue = event.returnValue; + const symbolError = errorName(() => { + event.returnValue = Symbol("return value"); + }); + + return JSON.stringify({ + constructor: [ + typeof BeforeUnloadEvent, + Object.getPrototypeOf(BeforeUnloadEvent.prototype) === Event.prototype, + errorName(() => new BeforeUnloadEvent()) + ], + initial, + initialized: [event.type, event.cancelable], + values: [nullValue, undefinedValue, objectValue, symbolError, event.returnValue], + metadata: [ + descriptor.get.name, + descriptor.get.length, + descriptor.set.name, + descriptor.set.length, + descriptor.enumerable, + descriptor.configurable + ], + receiverErrors: [ + errorName(() => descriptor.get.call(new Event("plain"))), + errorName(() => descriptor.set.call({}, "value")) + ] + }); + })() + "#, + ) + .expect("BeforeUnloadEvent returnValue interface probe should evaluate"); + + assert_eq!( + result, + r#"{"constructor":["function",true,"TypeError"],"initial":[true,true,false,""],"initialized":["beforeunload",true],"values":["null","undefined","object value","TypeError","object value"],"metadata":["get returnValue",0,"set returnValue",1,true,true],"receiverErrors":["TypeError","TypeError"]}"# + ); +} diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/navigation/navigate_event.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/navigation/navigate_event.rs index 22384f3ba2..41d53a6ff5 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/navigation/navigate_event.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/navigation/navigate_event.rs @@ -175,6 +175,59 @@ fn cross_document_unload_lifecycle_orders_pagehide_before_unload_without_timer() ); } +#[test] +fn before_unload_handler_coerces_its_result_while_window_event_is_current() { + let mut vm = new_storage_test_vm("https://example.com/base"); + + let result = vm + .eval( + r##" + (() => { + let customCurrent = false; + onbeforeunload = event => ({ + toString() { + customCurrent = window.event === event; + return "custom"; + } + }); + const custom = new CustomEvent("beforeunload", { cancelable: true }); + const customDispatchResult = dispatchEvent(custom); + + let realEvent; + const realSteps = []; + onbeforeunload = event => { + realEvent = event; + realSteps.push([ + event instanceof BeforeUnloadEvent, + event.cancelable, + event.returnValue, + window.event === event + ]); + return { + toString() { + realSteps.push(["coerce", window.event === event]); + return "leave"; + } + }; + }; + navigation.navigate("/next-document"); + + return JSON.stringify({ + custom: [customCurrent, custom.defaultPrevented, customDispatchResult], + realSteps, + real: [realEvent.defaultPrevented, realEvent.returnValue] + }); + })() + "##, + ) + .expect("beforeunload handler return processing should evaluate"); + + assert_eq!( + result, + r#"{"custom":[true,false,true],"realSteps":[[true,true,"",true],["coerce",true]],"real":[true,"leave"]}"# + ); +} + #[test] fn navigate_event_intercept_option_stringification_preserves_thrown_exception() { let mut vm = new_storage_test_vm("https://example.com/base");