diff --git a/moli-benchmark/wpt-cross-current/failed-cases.txt b/moli-benchmark/wpt-cross-current/failed-cases.txt index 22b4effbb6..350c55627f 100644 --- a/moli-benchmark/wpt-cross-current/failed-cases.txt +++ b/moli-benchmark/wpt-cross-current/failed-cases.txt @@ -3023,7 +3023,6 @@ old-tests/submission/Microsoft/selection/insertNodeIntoSelection.htm old-tests/submission/Microsoft/selection/select.htm old-tests/submission/Microsoft/selection/selectionStartEnd.htm old-tests/submission/Microsoft/selection/setSelectionRange.htm -page-visibility/iframe-session-history.html permissions-policy/experimental-features/focus-without-user-activation-disabled.html permissions-policy/experimental-features/focus-without-user-activation-focused-frame-descendant.html permissions-policy/experimental-features/focus-without-user-activation-setter-policy.html diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt index 1f42584e26..d856c0bcd5 100644 --- a/moli-benchmark/wpt-cross-current/passed-cases.txt +++ b/moli-benchmark/wpt-cross-current/passed-cases.txt @@ -8206,6 +8206,7 @@ old-tests/submission/Microsoft/selection/removeRange.htm old-tests/submission/Microsoft/selection/selectAllChildren.htm orientation-sensor/OrientationSensor_insecure_context.html page-lifecycle/idlharness.html +page-visibility/iframe-session-history.html page-visibility/test_attributes_exist.html page-visibility/test_child_document.html page-visibility/test_default_view.html diff --git a/moli-core/tests/history_visibility.rs b/moli-core/tests/history_visibility.rs new file mode 100644 index 0000000000..5f180f0a17 --- /dev/null +++ b/moli-core/tests/history_visibility.rs @@ -0,0 +1,233 @@ +use anyhow::Result; +use moli_core::runtime::{Browser, BrowserConfig}; +use moli_test_support::FixtureServer; +use serde_json::{Value, json}; +use tokio::time::Duration; +use url::Url; + +async fn history_visibility(flow: &str, api: &str, depth: usize) -> Result { + let child = r#"

child

"#; + let child = serde_json::to_string(child)?.replace("", "<\\/script>"); + let markup = r#""# + .replace("__CHILD__", &child) + .replace("__FLOW__", &serde_json::to_string(flow)?) + .replace("__API__", &serde_json::to_string(api)?) + .replace("__DEPTH__", &depth.to_string()); + let server = FixtureServer::spawn().await?; + let browser = Browser::new(BrowserConfig::default())?; + let mut url = Url::parse(&server.url("/compat/child-dynamic-markup-document"))?; + url.query_pairs_mut().append_pair("markup", &markup); + let result = tokio::time::timeout(Duration::from_secs(10), async { + let mut page = browser.fetch(url.as_str()).await?; + page.evaluate_runtime_expression_with_await_async( + "finished.then(value => JSON.stringify(value))", + true, + ) + .await + }) + .await??; + let result = serde_json::from_str(result["value"].as_str().unwrap())?; + server.shutdown().await; + Ok(result) +} + +#[tokio::test(flavor = "multi_thread")] +async fn native_window_event_targets_are_distinct_from_script_dispatch() -> Result<()> { + let result = history_visibility("synthetic", "history", 0).await?; + let native = result["native"].as_array().unwrap(); + assert_eq!(native.len(), 2, "{result}"); + for (event, kind) in native.iter().zip(["load", "pageshow"]) { + assert_eq!(event["type"], kind); + assert_eq!(event["target"], "document", "{result}"); + assert_eq!(event["currentWindow"], true); + assert_eq!(event["trusted"], true); + } + let synthetic = result["synthetic"].as_array().unwrap(); + assert_eq!(synthetic.len(), 5); + for event in synthetic { + assert_eq!(event["target"], "window", "{result}"); + assert_eq!(event["currentWindow"], true); + assert_eq!(event["trusted"], false, "{result}"); + } + assert_eq!(result["documentCalls"], 0); + assert_eq!(result["documentWindowEventCalls"], 0); + assert_eq!(result["wasTrusted"], true); + assert_eq!(result["completedBeforeRedispatch"], true); + assert_eq!(result["currentTargetCleared"], true); + assert_eq!(result["phase"], 0); + assert_eq!(result["hidden"], false); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn history_traversal_updates_visibility_and_unloads_descendants_once() -> Result<()> { + for api in ["history", "navigation"] { + for depth in [0, 2] { + let result = history_visibility("roundtrip", api, depth).await?; + assert_eq!(result["unrelatedUnchanged"], true); + for (phase, old, new, descendant_count) in + [("back", "B", "A", depth), ("forward", "A", "B", 0)] + { + let phase = &result[phase]; + assert_eq!(phase["oldHidden"], true, "{api}/{depth}: {result}"); + assert_eq!(phase["newHidden"], false); + assert_eq!(phase["sameDocument"], false); + let events = phase["events"].as_array().unwrap(); + let labels: Vec<_> = std::iter::once(old.to_owned()) + .chain((0..descendant_count).map(|n| format!("descendant-{n}"))) + .collect(); + assert!( + events + .iter() + .take(labels.len()) + .all(|e| e["type"] == "beforeunload"), + "{api}/{depth}: {result}" + ); + for label in &labels { + let actual: Vec<_> = events.iter().filter(|e| e["label"] == *label).collect(); + assert_eq!(actual.len(), 4, "{api}/{depth}: {result}"); + for (event, kind) in actual.iter().zip([ + "beforeunload", + "pagehide", + "visibilitychange", + "unload", + ]) { + assert_eq!(event["type"], kind, "{api}/{depth}: {result}"); + assert_eq!( + event["hidden"], + matches!(kind, "visibilitychange" | "unload") + ); + if kind != "beforeunload" { + assert_eq!(event["target"], "document", "{result}"); + } + assert_eq!(event["trusted"], true); + } + } + let loaded: Vec<_> = events.iter().filter(|e| e["label"] == new).collect(); + assert_eq!(loaded.len(), 2, "{api}/{depth}: {result}"); + for (event, kind) in loaded.iter().zip(["load", "pageshow"]) { + assert_eq!(event["type"], kind); + assert_eq!(event["target"], "document"); + assert_eq!(event["hidden"], false); + } + assert_eq!(events.len(), labels.len() * 4 + 2); + } + } + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn same_document_traversal_keeps_visibility_and_skips_unload() -> Result<()> { + for api in ["history", "navigation"] { + let result = history_visibility("same-document", api, 0).await?; + assert_eq!(result["events"], json!([]), "{api}: {result}"); + assert_eq!(result["sameDocument"], true); + assert_eq!(result["hidden"], false); + } + Ok(()) +} diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_events.rs b/moli-renderer-v8/src/context_bootstrap/navigation_events.rs index fc962a056f..a939a7cd7d 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_events.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_events.rs @@ -675,11 +675,12 @@ fn dispatch_unload_lifecycle_event_for_runtime_owner<'s>( } else if let Some(child_handle) = child_browsing_context_handle_for_runtime_owner(scope, owner) && let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) { - unsafe { &mut *host_ptr }.dispatch_child_window_event( + unsafe { &mut *host_ptr }.dispatch_child_window_event_with_target_override( scope, child_handle, event_type, event, + matches!(event_type, "pagehide" | "unload"), ); } set_navigation_unload_event_active(scope, owner, false); diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_traversal_execution.rs b/moli-renderer-v8/src/context_bootstrap/navigation_traversal_execution.rs index 46d0203219..4678adc3cc 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_traversal_execution.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_traversal_execution.rs @@ -8,8 +8,7 @@ use super::navigation_entry::{ }; use super::navigation_events::{ dispatch_beforeunload_for_runtime_owner, dispatch_navigation_traverse_event, - dispatch_navigation_traverse_event_with_outcome, dispatch_pagehide_for_runtime_owner, - dispatch_unload_for_runtime_owner, mark_navigation_outcome_default_prevented, + dispatch_navigation_traverse_event_with_outcome, mark_navigation_outcome_default_prevented, }; use super::navigation_lifecycle::finish_navigation_error_events; use super::navigation_result::{ @@ -339,7 +338,7 @@ fn dispatch_child_cross_document_traverse_event<'s>( info: Option>, ) -> bool { dispatch_beforeunload_for_runtime_owner(scope, target.owner); - let proceed = window_navigation_for_holder(scope, target.owner).is_none_or(|navigation| { + window_navigation_for_holder(scope, target.owner).is_none_or(|navigation| { let outcome = dispatch_navigation_traverse_event_with_outcome( scope, navigation, @@ -352,12 +351,7 @@ fn dispatch_child_cross_document_traverse_event<'s>( return false; } outcome.proceed - }); - if proceed { - dispatch_pagehide_for_runtime_owner(scope, target.owner); - dispatch_unload_for_runtime_owner(scope, target.owner); - } - proceed + }) } pub(in crate::context_bootstrap) fn apply_pending_child_cross_document_traversal( @@ -393,13 +387,25 @@ pub(in crate::context_bootstrap) fn apply_pending_child_cross_document_traversal .info .as_ref() .map(|info| v8::Local::new(scope, info)); + let retiring_document = host.child_browsing_context_document_handle(traversal.child_handle); if !dispatch_child_cross_document_traverse_event(scope, &target, info) { reject_child_cross_document_traversal(scope, &traversal); return; } - let _ = host.mark_current_child_document_unload_dispatched_after_navigation_traversal( - traversal.child_handle, - ); + if retiring_document.is_none() + || host.child_browsing_context_document_handle(traversal.child_handle) != retiring_document + || window_task_target_for_runtime_owner(scope, host, owner) != Some(traversal.target) + { + reject_child_cross_document_traversal(scope, &traversal); + return; + } + host.dispatch_child_document_unload_after_traversal_check(scope, traversal.child_handle); + if host.child_browsing_context_document_handle(traversal.child_handle) != retiring_document + || window_task_target_for_runtime_owner(scope, host, owner) != Some(traversal.target) + { + reject_child_cross_document_traversal(scope, &traversal); + return; + } queue_child_cross_document_traversal( host, traversal.child_handle, diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents/lifecycle.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents/lifecycle.rs index ac6ed6f158..785087536c 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_documents/lifecycle.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents/lifecycle.rs @@ -852,18 +852,14 @@ impl JsContextHost { } } - pub(crate) fn mark_current_child_document_unload_dispatched_after_navigation_traversal( + pub(crate) fn dispatch_child_document_unload_after_traversal_check( &mut self, + scope: &mut v8::PinScope<'_, '_>, handle: DomHandle, ) -> bool { - let Some(action) = self - .frame_owner_store - .begin_current_child_document_unload(handle) - else { - return false; - }; - self.frame_owner_store - .finish_current_child_document_unload(action) + // Traversal has already fired the root beforeunload before navigate. + // Descendants still need their checks before any document unloads. + self.dispatch_child_document_tree_unload_lifecycle(scope, handle, false) } pub(in crate::native_bridge::context_host) fn dispatch_child_browsing_context_unload_lifecycle_if_needed( @@ -871,6 +867,16 @@ impl JsContextHost { scope: &mut v8::PinScope<'_, '_>, handle: DomHandle, ) -> bool { + self.dispatch_child_document_tree_unload_lifecycle(scope, handle, true) + } + + fn dispatch_child_document_tree_unload_lifecycle( + &mut self, + scope: &mut v8::PinScope<'_, '_>, + handle: DomHandle, + include_root_beforeunload: bool, + ) -> bool { + let root_handle = handle; let documents = self.child_document_unload_tree_snapshot(handle); let mut unload_guards = Vec::new(); let mut actions = Vec::new(); @@ -904,7 +910,9 @@ impl JsContextHost { }; unload_guards.push((document, self.enter_document_unload(document))); actions.push((document, parent_document, action)); - dispatch_beforeunload_for_runtime_owner(scope, window); + if include_root_beforeunload || handle != root_handle { + dispatch_beforeunload_for_runtime_owner(scope, window); + } } unload_guards.clear(); let dispatched = !actions.is_empty(); 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 5c616f6058..901be145e3 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 @@ -480,6 +480,19 @@ impl JsContextHost { handle: DomHandle, event_type: &str, event: v8::Local<'s, v8::Object>, + ) { + self.dispatch_child_window_event_with_target_override( + scope, handle, event_type, event, false, + ); + } + + pub(crate) fn dispatch_child_window_event_with_target_override<'s>( + &mut self, + scope: &mut v8::PinScope<'s, '_>, + handle: DomHandle, + event_type: &str, + event: v8::Local<'s, v8::Object>, + legacy_target_override: bool, ) { if !self.child_window_event_requires_runtime_dispatch(handle, event_type) { return; @@ -492,7 +505,9 @@ impl JsContextHost { }; let previous_active_child_window = enter_child_window_event_dispatch(scope, handle); self.push_child_subresource_request_scope(handle); - let target = if event_type == "unload" { + // The legacy flag changes event.target, while dispatch still takes + // place at Window. Script dispatch never sets this flag. + let target = if legacy_target_override { self.child_browsing_context_document_wrapper(scope, handle) .map(Into::into) .unwrap_or_else(|| window.into()) diff --git a/moli-renderer-v8/src/native_bridge/context_host/host_loads.rs b/moli-renderer-v8/src/native_bridge/context_host/host_loads.rs index eb0a80641a..24a8c33e1a 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/host_loads.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/host_loads.rs @@ -417,7 +417,7 @@ impl JsContextHost { record_performance_load_event_start_for_window(scope, window); } self.enter_child_browsing_context_host_load_dispatch(handle); - self.dispatch_child_window_event(scope, handle, "load", event); + self.dispatch_child_window_event_with_target_override(scope, handle, "load", event, true); self.leave_child_browsing_context_host_load_dispatch(handle); if let Some(window) = performance_window { record_performance_load_event_end_for_window(scope, window); @@ -473,7 +473,13 @@ impl JsContextHost { let callback_dispatched = if let Some(event) = construct_original_page_transition_event(scope, "pageshow", false) { - self.dispatch_child_window_event(scope, action.child_handle(), "pageshow", event); + self.dispatch_child_window_event_with_target_override( + scope, + action.child_handle(), + "pageshow", + event, + true, + ); true } else { false diff --git a/moli-renderer-v8/src/window_host.rs b/moli-renderer-v8/src/window_host.rs index 97a230c498..b164549edf 100644 --- a/moli-renderer-v8/src/window_host.rs +++ b/moli-renderer-v8/src/window_host.rs @@ -9,6 +9,7 @@ use super::{ context_bootstrap::increment_performance_event_count, context_bootstrap::mark_event_trusted, context_bootstrap::performance_slot_number, + context_bootstrap::set_event_trusted, context_bootstrap::simple_event_target_add_event_listener_callback, context_bootstrap::simple_event_target_dispatch_event_callback, context_bootstrap::simple_event_target_remove_event_listener_callback, @@ -500,6 +501,7 @@ pub(super) fn event_target_dispatch_event_callback<'s>( return; } + set_event_trusted(scope, event, false); 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();