fix(window): expose native closed accessors

This commit is contained in:
ldm0
2026-09-30 04:04:08 +08:00
parent d92d4da858
commit d728dbfbee
5 changed files with 111 additions and 6 deletions
@@ -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,
};
@@ -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)
})
}
@@ -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));
}
@@ -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: (),
@@ -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/", "<!doctype html><body>");
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]");
}