diff --git a/moli-benchmark/wpt-cross-current/failed-cases.txt b/moli-benchmark/wpt-cross-current/failed-cases.txt index 0290ef22ef..429c2fec05 100644 --- a/moli-benchmark/wpt-cross-current/failed-cases.txt +++ b/moli-benchmark/wpt-cross-current/failed-cases.txt @@ -3155,7 +3155,6 @@ html/semantics/interactive-elements/the-dialog-element/dialog-focusing-steps-ine html/semantics/interactive-elements/the-dialog-element/dialog-focusing-steps-prevent-autofocus.html html/semantics/interactive-elements/the-dialog-element/dialog-inert.html html/semantics/interactive-elements/the-dialog-element/dialog-overlay.html -html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-recurse.html html/semantics/interactive-elements/the-dialog-element/dialog-showModal.html html/semantics/interactive-elements/the-dialog-element/inertness-with-modal-dialogs-and-iframes.html html/semantics/interactive-elements/the-dialog-element/modal-dialog-in-visibility-hidden.html diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt index b2f085a8e9..567bcf09c9 100644 --- a/moli-benchmark/wpt-cross-current/passed-cases.txt +++ b/moli-benchmark/wpt-cross-current/passed-cases.txt @@ -5951,6 +5951,7 @@ html/semantics/interactive-elements/the-dialog-element/dialog-form-submission-un html/semantics/interactive-elements/the-dialog-element/dialog-no-throw-requested-state.html html/semantics/interactive-elements/the-dialog-element/dialog-open-2.html html/semantics/interactive-elements/the-dialog-element/dialog-open.html +html/semantics/interactive-elements/the-dialog-element/dialog-requestclose-recurse.html html/semantics/interactive-elements/the-dialog-element/dialog-return-value.html html/semantics/interactive-elements/the-dialog-element/dialog-showModal-remove.html html/semantics/interactive-elements/the-dialog-element/form-submit-dialog-shadow.html diff --git a/moli-renderer-v8/src/native_bridge/context_host/core.rs b/moli-renderer-v8/src/native_bridge/context_host/core.rs index f92a1e7ff8..5640993d84 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/core.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/core.rs @@ -387,6 +387,7 @@ impl JsContextHost { button_element_targets: HashMap::new(), constructing_form_data_forms: Vec::new(), active_form_submission_forms: Vec::new(), + active_dialog_request_closes: HashSet::new(), pending_form_submission_child_targets: HashMap::new(), active_image_submitter_coordinate: None, current_inline_script_stack: Vec::new(), @@ -1704,6 +1705,14 @@ impl JsContextHost { } } + pub(crate) fn begin_dialog_request_close(&mut self, dialog: DomHandle) -> bool { + self.active_dialog_request_closes.insert(dialog) + } + + pub(crate) fn end_dialog_request_close(&mut self, dialog: DomHandle) { + self.active_dialog_request_closes.remove(&dialog); + } + pub(crate) fn observers_mut( &mut self, _access: &ObserverStoreAccessToken, diff --git a/moli-renderer-v8/src/native_bridge/context_host/mod.rs b/moli-renderer-v8/src/native_bridge/context_host/mod.rs index f5faf452a3..570ae4651a 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/mod.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/mod.rs @@ -993,6 +993,7 @@ pub(crate) struct JsContextHost { button_element_targets: HashMap<(DomHandle, String), DomHandle>, constructing_form_data_forms: Vec, active_form_submission_forms: Vec, + active_dialog_request_closes: HashSet, pending_form_submission_child_targets: HashMap>, active_image_submitter_coordinate: Option<(DomHandle, u32, u32)>, current_inline_script_stack: Vec, diff --git a/moli-renderer-v8/src/native_bridge/element.rs b/moli-renderer-v8/src/native_bridge/element.rs index 3a7eba9e4b..4a3c850205 100644 --- a/moli-renderer-v8/src/native_bridge/element.rs +++ b/moli-renderer-v8/src/native_bridge/element.rs @@ -178,8 +178,9 @@ pub(super) use dataset::{build_dom_string_map_wrapper_template, node_dataset_get use details_dialog::{ close_dialog_element, details_open_getter_function, details_open_setter_function, dialog_close_callback, dialog_open_getter_function, dialog_open_setter_function, - dialog_return_value_getter_function, dialog_return_value_setter_function, dialog_show_callback, - dialog_show_modal_callback, perform_summary_click_default_action, + dialog_request_close_callback, dialog_return_value_getter_function, + dialog_return_value_setter_function, dialog_show_callback, dialog_show_modal_callback, + perform_summary_click_default_action, }; pub(crate) use details_dialog::{ queue_details_toggle_event_for_attribute_change, queue_parser_details_toggle_event, @@ -4378,6 +4379,8 @@ struct HtmlDialogElementPrototypeDeclaration { show_modal: (), #[webapi(method, length = 1, callback = dialog_close_callback)] close: (), + #[webapi(method, length = 1, callback = dialog_request_close_callback)] + request_close: (), } #[derive(WebApiFunctionTemplate)] diff --git a/moli-renderer-v8/src/native_bridge/element/details_dialog.rs b/moli-renderer-v8/src/native_bridge/element/details_dialog.rs index e4cbe3cb57..93503994bd 100644 --- a/moli-renderer-v8/src/native_bridge/element/details_dialog.rs +++ b/moli-renderer-v8/src/native_bridge/element/details_dialog.rs @@ -16,8 +16,9 @@ use super::super::{ use super::popover::popover_is_open; use super::toggle_event::queue_element_toggle_event; use super::{ - dispatch_public_event, element_has_attribute, html_element_getter_receiver, - html_element_setter_receiver, property_dom_string_value, set_reflected_boolean_attribute, + construct_simple_event, dispatch_public_event, element_has_attribute, + html_element_getter_receiver, html_element_setter_receiver, property_dom_string_value, + set_reflected_boolean_attribute, }; pub(crate) fn queue_details_toggle_event_for_attribute_change( @@ -101,6 +102,13 @@ struct DialogCloseArgs { return_value: Option, } +#[derive(webidl::WebIdlArgs)] +#[webidl(prefix = "HTMLDialogElement.requestClose")] +struct DialogRequestCloseArgs { + #[webidl(with = dialog_request_close_return_value_arg)] + return_value: Option, +} + pub(super) fn main_summary_child(runtime: &JsContextHost, details: DomHandle) -> Option { let details_element = runtime .dom_host() @@ -180,17 +188,29 @@ fn dialog_close_return_value_arg<'s>( scope: &mut v8::PinScope<'s, '_>, args: &v8::FunctionCallbackArguments<'s>, index: i32, +) -> Result, webidl::WebIdlError> { + dialog_optional_return_value_arg(scope, args, index, "HTMLDialogElement.close") +} + +fn dialog_request_close_return_value_arg<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: &v8::FunctionCallbackArguments<'s>, + index: i32, +) -> Result, webidl::WebIdlError> { + dialog_optional_return_value_arg(scope, args, index, "HTMLDialogElement.requestClose") +} + +fn dialog_optional_return_value_arg<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: &v8::FunctionCallbackArguments<'s>, + index: i32, + prefix: &'static str, ) -> Result, webidl::WebIdlError> { if args.length() <= index || args.get(index).is_undefined() { return Ok(None); } - webidl::argument::( - scope, - args, - index, - webidl::Context::argument("HTMLDialogElement.close", 1), - ) - .map(|value| Some(value.0)) + webidl::argument::(scope, args, index, webidl::Context::argument(prefix, 1)) + .map(|value| Some(value.0)) } fn dialog_runtime_and_handle_from_object<'s>( @@ -509,6 +529,38 @@ pub(super) fn dialog_close_callback<'s>( close_dialog_element(scope, runtime_ptr, handle, parsed.return_value.as_deref()); } +pub(super) fn dialog_request_close_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + _rv: v8::ReturnValue<'_, v8::Value>, +) { + let object = args.this(); + let Some(parsed) = webidl::parse_args::(scope, &args) else { + return; + }; + let Ok((runtime_ptr, handle)) = dialog_runtime_and_handle_from_object(scope, object) else { + return; + }; + let runtime = unsafe { &*runtime_ptr }; + if !runtime.dom_host().is_html_element_named(handle, "dialog") + || !runtime.dom_host().is_connected(handle) + || !element_has_attribute(runtime, handle, "open") + { + return; + } + let Some(event) = construct_simple_event(scope, "cancel", false, true, false) else { + return; + }; + if !unsafe { &mut *runtime_ptr }.begin_dialog_request_close(handle) { + return; + } + let allows_close = dispatch_public_event(scope, runtime_ptr, handle, event).allows_default(); + if allows_close { + close_dialog_element(scope, runtime_ptr, handle, parsed.return_value.as_deref()); + } + unsafe { &mut *runtime_ptr }.end_dialog_request_close(handle); +} + pub(in crate::native_bridge::element) fn close_dialog_element( scope: &mut v8::PinScope<'_, '_>, runtime_ptr: *mut JsContextHost, diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/forms/dialog.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/forms/dialog.rs index e3d712db41..b6a4e96d3f 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/forms/dialog.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/forms/dialog.rs @@ -70,6 +70,120 @@ async fn dialog_toggle_events_cancel_opening_and_coalesce_state_changes() { ); } +#[tokio::test] +async fn dialog_request_close_ignores_recursive_cancel_and_queues_close() { + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); + let mut vm = new_storage_page_task_executor_test_vm_with_loader( + "https://dialog-request-close-recursive.test/", + &loader, + ); + + let before_close = vm + .eval( + r#" +(() => { + const host = document.body || document.documentElement || document; + const dialog = document.createElement('dialog'); + host.appendChild(dialog); + globalThis.__dialogRequestCloseEvents = []; + let cancelCount = 0; + dialog.addEventListener('cancel', event => { + cancelCount += 1; + __dialogRequestCloseEvents.push(`${event.type}:${event.bubbles}:${event.cancelable}`); + dialog.requestClose('nested'); + }); + dialog.addEventListener('close', event => { + __dialogRequestCloseEvents.push(`${event.type}:${event.bubbles}:${event.cancelable}`); + }); + + dialog.showModal(); + dialog.requestClose('outer'); + return JSON.stringify({ + length: dialog.requestClose.length, + cancelCount, + open: dialog.open, + returnValue: dialog.returnValue, + events: __dialogRequestCloseEvents + }); +})() +"#, + ) + .expect("recursive dialog requestClose setup should evaluate"); + + assert_eq!( + before_close, + r#"{"length":1,"cancelCount":1,"open":false,"returnValue":"outer","events":["cancel:false:true"]}"# + ); + + assert!( + !vm.has_ready_timeout(), + "dialog requestClose must not create synthetic Page timers" + ); + assert!( + vm.run_one_dom_manipulation_task_executor_turn( + PageDomManipulationTestFamily::ElementToggle, + &loader, + ) + .await + .expect("coalesced dialog toggle event should run") + ); + assert!( + vm.run_one_user_interaction_executor_turn(&loader) + .await + .expect("queued dialog close event should run") + ); + let after_close = vm + .eval("JSON.stringify(__dialogRequestCloseEvents)") + .expect("dialog requestClose event log should evaluate"); + assert_eq!(after_close, r#"["cancel:false:true","close:false:false"]"#); +} + +#[test] +fn dialog_request_close_honors_cancellation_and_active_document() { + let mut vm = new_storage_test_vm("https://dialog-request-close-state.test/"); + + let result = vm + .eval( + r#" +(() => { + const host = document.body || document.documentElement || document; + const dialog = document.createElement('dialog'); + host.appendChild(dialog); + dialog.returnValue = 'seed'; + dialog.show(); + dialog.addEventListener('cancel', event => event.preventDefault(), { once: true }); + dialog.requestClose('blocked'); + const canceled = [dialog.open, dialog.returnValue]; + dialog.requestClose('accepted'); + const accepted = [dialog.open, dialog.returnValue]; + + const disconnected = document.createElement('dialog'); + disconnected.open = true; + disconnected.requestClose('disconnected'); + + const inactiveDocument = document.implementation.createHTMLDocument(''); + const inactive = inactiveDocument.createElement('dialog'); + inactiveDocument.body.appendChild(inactive); + inactive.open = true; + inactive.requestClose('inactive'); + + return JSON.stringify({ + canceled, + accepted, + disconnected: [disconnected.open, disconnected.returnValue], + inactive: [inactive.open, inactive.returnValue] + }); +})() +"#, + ) + .expect("dialog requestClose state probe should evaluate"); + + assert_eq!( + result, + r#"{"canceled":[true,"seed"],"accepted":[false,"accepted"],"disconnected":[true,""],"inactive":[true,""]}"# + ); +} + #[test] fn dialog_show_methods_enforce_requested_state_and_active_document() { let mut vm = new_storage_test_vm("https://dialog-requested-state.test/");