From d728dbfbeeff4ed398dc608436ca4c1763afb590 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Wed, 30 Sep 2026 04:04:08 +0800 Subject: [PATCH] fix(window): expose native closed accessors --- .../src/context_bootstrap/window_accessors.rs | 10 ++-- .../window_accessors/helpers.rs | 24 ++++++++ .../window_accessors/surface.rs | 21 ++++++- .../src/context_bootstrap/window_template.rs | 3 + .../src/script_vm/tests/dom_xhr/misc.rs | 59 +++++++++++++++++++ 5 files changed, 111 insertions(+), 6 deletions(-) diff --git a/moli-renderer-v8/src/context_bootstrap/window_accessors.rs b/moli-renderer-v8/src/context_bootstrap/window_accessors.rs index 9c97181e92..b9de5fe84c 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_accessors.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_accessors.rs @@ -28,11 +28,11 @@ pub(super) use interceptors::{ window_named_property_query, }; pub(super) use surface::{ - window_custom_elements_getter, window_device_pixel_ratio_getter, window_frames_getter, - window_inner_height_getter, window_inner_surface_height, window_inner_surface_width, - window_inner_width_getter, window_navigator_getter, window_opener_getter, - window_outer_height_getter, window_outer_width_getter, window_parent_getter, - window_performance_getter, window_screen_getter, window_scroll_x_getter, + window_closed_getter, window_custom_elements_getter, window_device_pixel_ratio_getter, + window_frames_getter, window_inner_height_getter, window_inner_surface_height, + window_inner_surface_width, window_inner_width_getter, window_navigator_getter, + window_opener_getter, window_outer_height_getter, window_outer_width_getter, + window_parent_getter, window_performance_getter, window_screen_getter, window_scroll_x_getter, window_scroll_y_getter, window_self_getter, window_speech_synthesis_getter, window_top_getter, window_visual_viewport_getter, window_window_getter, }; diff --git a/moli-renderer-v8/src/context_bootstrap/window_accessors/helpers.rs b/moli-renderer-v8/src/context_bootstrap/window_accessors/helpers.rs index e83f16e785..19860fa70a 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_accessors/helpers.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_accessors/helpers.rs @@ -123,3 +123,27 @@ pub(crate) fn current_window_style_viewport( }) .unwrap_or_else(|| host.style_viewport()) } + +pub(in crate::context_bootstrap) fn window_has_discarded_child_browsing_context<'s>( + scope: &mut v8::PinScope<'s, '_>, + receiver: v8::Local<'s, v8::Object>, +) -> bool { + let Some(handle) = window_child_context_handle(scope, receiver) else { + return false; + }; + let Some(host_ptr) = window_host_ptr(scope, receiver) else { + return true; + }; + let host = unsafe { &*host_ptr }; + // Container removal disconnects the DOM node before its unload callbacks. + // The captured LocalWindow remains current until those callbacks finish; + // checking its identity also keeps reattachment from reviving an old realm. + let Some(context) = receiver.get_creation_context(scope) else { + return true; + }; + host.window_execution_context_identity_for_access_check(context) + .is_none_or(|identity| { + identity.dispatch_scope() != crate::native_bridge::OwnerDispatchScope::Child(handle) + || !host.window_execution_context_identity_is_current(identity) + }) +} diff --git a/moli-renderer-v8/src/context_bootstrap/window_accessors/surface.rs b/moli-renderer-v8/src/context_bootstrap/window_accessors/surface.rs index 9c2f4ac531..71a0ea64be 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_accessors/surface.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_accessors/surface.rs @@ -1,5 +1,6 @@ use super::helpers::{ - window_child_context_handle, window_hidden_value, window_host_ptr, window_receiver, + window_child_context_handle, window_has_discarded_child_browsing_context, window_hidden_value, + window_host_ptr, window_receiver, }; use super::*; @@ -296,3 +297,21 @@ pub(in crate::context_bootstrap) fn window_self_getter<'s>( ) { set_receiver_window_alias(scope, &args, WINDOW_SELF_SLOT, rv); } + +pub(in crate::context_bootstrap) fn window_closed_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let Some(receiver) = window_receiver(scope, &args) else { + return; + }; + if let Some(popup_id) = crate::native_bridge::lightweight_popup_id_from_window(scope, receiver) + { + let closed = window_host_ptr(scope, receiver) + .is_none_or(|host_ptr| !unsafe { &*host_ptr }.lightweight_popup_is_open(popup_id)); + rv.set_bool(closed); + return; + } + rv.set_bool(window_has_discarded_child_browsing_context(scope, receiver)); +} diff --git a/moli-renderer-v8/src/context_bootstrap/window_template.rs b/moli-renderer-v8/src/context_bootstrap/window_template.rs index 9c909d00a8..fe51a8559e 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_template.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_template.rs @@ -190,6 +190,9 @@ struct WindowIdentityAccessorsDeclaration { #[webapi(accessor_property, getter = window_frames_getter)] frames: (), + #[webapi(accessor_property, getter = window_closed_getter)] + closed: (), + #[webapi(accessor_property, getter = window_frame_element_getter)] frame_element: (), diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/misc.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/misc.rs index 3a2ea2cd98..958d3e20e8 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/misc.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/misc.rs @@ -2026,3 +2026,62 @@ container.querySelector('a').click(); assert_eq!(result, "true|true|true"); } +#[test] +fn window_closed_reflects_top_popup_and_popup_child_liveness() { + let mut vm = new_storage_test_vm("https://window-closed.test/"); + + let result = vm + .eval( + r#" +(() => { + const popup = open(); + popup.document.body.appendChild(popup.document.createElement("iframe")); + const child = popup[0]; + const descriptor = Object.getOwnPropertyDescriptor(window, "closed"); + const before = [window.closed, popup.closed, child.closed]; + popup.close(); + return JSON.stringify({ + descriptor: [ + descriptor.get.name, + descriptor.get.length, + typeof descriptor.set, + descriptor.enumerable, + descriptor.configurable + ], + before, + after: [ + popup.closed, + child.closed, + descriptor.get.call(popup), + descriptor.get.call(child) + ] + }); +})() +"#, + ) + .expect("Window.closed liveness probe should evaluate"); + + assert_eq!( + result, + r#"{"descriptor":["get closed",0,"undefined",true,true],"before":[false,false,false],"after":[true,false,true,false]}"# + ); +} + +#[test] +fn window_closed_reflects_removed_iframe_state() { + let mut vm = new_parsed_test_vm("https://child-window-closed.test/", ""); + let result = vm + .eval( + r#"(() => { + const frame = document.body.appendChild(document.createElement('iframe')); + const child = frame.contentWindow; + const getter = Object.getOwnPropertyDescriptor(window, 'closed').get; + const states = [child.closed, getter.call(child)]; + frame.remove(); + states.push(child.closed, getter.call(child)); + return JSON.stringify(states); + })()"#, + ) + .expect("closed should reflect iframe removal through the native getter"); + assert_eq!(result, "[false,false,true,true]"); +}