diff --git a/moli-benchmark/wpt-cross-current/failed-cases.txt b/moli-benchmark/wpt-cross-current/failed-cases.txt index a1afaf4707..d6304108f0 100644 --- a/moli-benchmark/wpt-cross-current/failed-cases.txt +++ b/moli-benchmark/wpt-cross-current/failed-cases.txt @@ -2722,9 +2722,6 @@ html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/l html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-setter-during-load.html html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-setter-during-pageshow.html html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/location-setter.html -html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-cross-document-nav.html -html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-cross-document-traversal.html -html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-same-document-nav.html html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-same-document-traversal.html html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-cross-document-traversal.html html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-same-document-traversal.html diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt index 918ecfa879..7f4b9b2609 100644 --- a/moli-benchmark/wpt-cross-current/passed-cases.txt +++ b/moli-benchmark/wpt-cross-current/passed-cases.txt @@ -5293,6 +5293,9 @@ html/browsers/browsing-the-web/navigating-across-documents/source/navigate-child html/browsers/browsing-the-web/navigating-across-documents/source/navigate-child-function.html html/browsers/browsing-the-web/navigating-across-documents/source/navigate-child-src-about-blank.html html/browsers/browsing-the-web/overlapping-navigations-and-traversals/anchor-fragment-history-back-on-click.html +html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-cross-document-nav.html +html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-cross-document-traversal.html +html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-same-document-nav.html html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-nav-stop.html html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-same-document-nav.html html/browsers/browsing-the-web/overlapping-navigations-and-traversals/cross-document-traversal-stop.html diff --git a/moli-core/tests/history_child.rs b/moli-core/tests/history_child.rs index 1269670ecf..82a5ef8ec5 100644 --- a/moli-core/tests/history_child.rs +++ b/moli-core/tests/history_child.rs @@ -334,7 +334,7 @@ async fn assert_child_location_navigation_stays_window_local( page.serialize_html_async() .await .unwrap() - .contains(&format!("data-child-current-entry-url=\"{}\"", final_url)), + .contains(&format!("data-child-current-entry-url=\"{}\"", initial_url)), "{}", page.serialize_html_async().await.unwrap() ); @@ -342,7 +342,7 @@ async fn assert_child_location_navigation_stays_window_local( page.serialize_html_async() .await .unwrap() - .contains(&format!("data-child-location-href=\"{}\"", final_url)), + .contains(&format!("data-child-location-href=\"{}\"", initial_url)), "{}", page.serialize_html_async().await.unwrap() ); @@ -352,7 +352,7 @@ async fn assert_child_location_navigation_stays_window_local( .unwrap() .contains(&format!( "data-child-document-location-immediate=\"{}\"", - final_url + initial_url )), "{}", page.serialize_html_async().await.unwrap() @@ -438,7 +438,7 @@ async fn assert_child_location_navigation_stays_window_local( page.serialize_html_async() .await .unwrap() - .contains("data-child-location-target-pending-microtask=\"true\"") + .contains("data-child-location-target-pending-microtask=\"false\"") ); assert!( page.serialize_html_async() @@ -446,7 +446,7 @@ async fn assert_child_location_navigation_stays_window_local( .unwrap() .contains(&format!( "data-child-document-location-pending-microtask=\"{}\"", - final_url + initial_url )), "{}", page.serialize_html_async().await.unwrap() @@ -526,6 +526,15 @@ async fn assert_child_location_navigation_stays_window_local( .unwrap() .contains("data-child-document-text-after-load=\"delayed\"") ); + assert_eq!( + evaluated_string( + page.evaluate_runtime_expression_async( + "document.getElementById('child').contentWindow.navigation.currentEntry.url", + ) + .await? + ), + Some(final_url) + ); Ok(()) } @@ -700,14 +709,14 @@ async fn assert_child_location_component_keeps_committed_document_until_load( "data-document-default-view-same-immediate=\"true\"", "data-default-view-document-same-immediate=\"true\"", "data-child-document-still-committed-immediate=\"true\"", - "data-child-location-target-immediate=\"true\"", + "data-child-location-target-immediate=\"false\"", "data-window-same-pending-microtask=\"true\"", "data-document-same-pending-microtask=\"true\"", "data-window-document-same-pending-microtask=\"true\"", "data-document-default-view-same-pending-microtask=\"true\"", "data-default-view-document-same-pending-microtask=\"true\"", "data-child-document-still-committed-pending-microtask=\"true\"", - "data-child-location-target-pending-microtask=\"true\"", + "data-child-location-target-pending-microtask=\"false\"", "data-document-replaced-after-load=\"true\"", ] { assert!(html.contains(attr), "{attr}\n{html}"); @@ -738,8 +747,11 @@ async fn assert_child_location_component_keeps_committed_document_until_load( assert_html_contains_attr(&html, "data-child-document-text-after-load", final_text); } for attr in [ - ("data-child-location-immediate", final_url), - ("data-child-location-pending-microtask", final_url), + ("data-child-location-immediate", initial_url.as_str()), + ( + "data-child-location-pending-microtask", + initial_url.as_str(), + ), ("data-child-location-after-load", final_url), ("data-child-document-url-immediate", &initial_url), ("data-child-document-url-pending-microtask", &initial_url), @@ -749,19 +761,19 @@ async fn assert_child_location_component_keeps_committed_document_until_load( if !final_is_cross_origin { assert_html_contains_attr(&html, "data-child-document-url-after-load", final_url); } - for attr in [ - "data-child-pathname-immediate", - "data-child-pathname-pending-microtask", - "data-child-pathname-after-load", + let initial_pathname = Url::parse(&initial_url)?.path().to_owned(); + for (attr, value) in [ + ("data-child-pathname-immediate", initial_pathname.as_str()), + ( + "data-child-pathname-pending-microtask", + initial_pathname.as_str(), + ), + ("data-child-pathname-after-load", final_pathname), + ("data-child-search-immediate", ""), + ("data-child-search-pending-microtask", ""), + ("data-child-search-after-load", final_search), ] { - assert_html_contains_attr(&html, attr, final_pathname); - } - for attr in [ - "data-child-search-immediate", - "data-child-search-pending-microtask", - "data-child-search-after-load", - ] { - assert_html_contains_attr(&html, attr, final_search); + assert_html_contains_attr(&html, attr, value); } Ok(()) diff --git a/moli-renderer-v8/src/context_bootstrap/location_navigation.rs b/moli-renderer-v8/src/context_bootstrap/location_navigation.rs index 00c3b1d86d..65bbf8ba51 100644 --- a/moli-renderer-v8/src/context_bootstrap/location_navigation.rs +++ b/moli-renderer-v8/src/context_bootstrap/location_navigation.rs @@ -23,8 +23,8 @@ use super::navigation_lifecycle::{ settle_navigation_transition_finished_local, }; use super::navigation_mutation::{ - apply_local_window_location_navigation, apply_navigation_navigate_same_document, - sync_local_document_front_from_window, update_navigation_current_entry_for_same_document, + apply_navigation_navigate_same_document, sync_local_document_front_from_window, + update_navigation_current_entry_for_same_document, }; use super::navigation_reload::{NavigationReloadAdmission, navigation_reload_admission}; use super::navigation_result::{ @@ -670,29 +670,34 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>( { return; } - if !is_javascript_url { - sync_location_object(scope, location, resolved.as_str()); - apply_local_window_location_navigation(scope, owner, &resolved, kind); - } if let Some(host_ptr) = host_ptr { let host = unsafe { &mut *host_ptr }; let initiator_url = explicit_initiator_url.or_else(|| location_navigation_initiator_url(scope, host)); - if matches!(kind, LocationNavigationKind::Assign) && !is_javascript_url { - host.mark_child_browsing_context_top_level_history_increment(handle); - } - if matches!(kind, LocationNavigationKind::Reload) { - host.queue_child_browsing_context_reload_from_existing_seed( - handle, - resolved.as_str(), - initiator_url, - ); - } else { + if is_javascript_url { host.queue_child_browsing_context_navigation_without_seed_update( handle, resolved.as_str(), initiator_url, ); + } else { + // A cross-document navigation only prepares the next history + // entry. The old Document's Location, history state and entry + // identity remain visible until the replacement commits. + let entry_seed = if matches!(kind, LocationNavigationKind::Reload) { + history_entry_seed_for_reload(scope, owner) + } else { + history_entry_seed_for_cross_document_location(scope, owner, &resolved, kind) + }; + if let Some(entry_seed) = entry_seed { + host.queue_deferred_child_browsing_context_navigation_from_entry_seed( + handle, + resolved.as_str(), + entry_seed, + matches!(kind, LocationNavigationKind::Assign), + initiator_url, + ); + } } } return; diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_cancellation.rs b/moli-renderer-v8/src/context_bootstrap/navigation_cancellation.rs index 7726c393df..622526fe71 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_cancellation.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_cancellation.rs @@ -10,7 +10,7 @@ use super::{ cancel_pending_same_document_navigation_finishes_including_reentrant, }, navigation_window::{ - runtime_window_is_global, runtime_window_owner, window_navigation_for_holder, + runtime_window_dispatch_scope, runtime_window_owner, window_navigation_for_holder, }, }; use crate::util::context_host_ptr_from_global_bridge; @@ -29,9 +29,24 @@ pub(crate) fn inform_about_canceled_navigation_for_window<'s>( cancel_pending_precommit_history_traversal(scope, navigation); cancel_pending_precommit_same_document_navigation_for_window_stop(scope, navigation); cancel_pending_same_document_navigation_finishes_including_reentrant(scope, navigation); - if runtime_window_is_global(scope, owner) - && let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) - { - unsafe { &mut *host_ptr }.clear_pending_location_navigation(); + clear_pending_cross_document_navigation_for_window(scope, owner); +} + +pub(super) fn clear_pending_cross_document_navigation_for_window<'s>( + scope: &mut v8::PinScope<'s, '_>, + window: v8::Local<'s, v8::Object>, +) { + let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + return; + }; + let host = unsafe { &mut *host_ptr }; + match runtime_window_dispatch_scope(scope, window) { + Some(crate::native_bridge::OwnerDispatchScope::Top) => { + host.clear_pending_location_navigation(); + } + Some(crate::native_bridge::OwnerDispatchScope::Child(handle)) => { + host.cancel_pending_child_browsing_context_navigation(handle); + } + _ => {} } } diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_cross_document.rs b/moli-renderer-v8/src/context_bootstrap/navigation_cross_document.rs index 61f09550c7..fbde6b4251 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_cross_document.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_cross_document.rs @@ -114,8 +114,8 @@ pub(super) fn handle_navigation_navigate_cross_document<'s>( next_url.as_str(), entry_seed, matches!(mutation, NavigationHistoryMutation::Push), + None, ); - host.sync_existing_child_browsing_context_window_state(scope, child_handle); navigation_signal .map(|(navigation, signal)| { navigation_cross_document_pending_result(scope, navigation, signal, next_url.as_str()) diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_result.rs b/moli-renderer-v8/src/context_bootstrap/navigation_result.rs index 261fe3580f..192c714d2d 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_result.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_result.rs @@ -352,6 +352,10 @@ pub(super) fn cancel_active_cross_document_navigation<'s>( v8::Boolean::new(scope, false).into(), ); clear_active_cross_document_navigation(scope, navigation); + let owner = super::navigation_window::runtime_window_owner(scope, navigation); + super::navigation_cancellation::clear_pending_cross_document_navigation_for_window( + scope, owner, + ); let error = navigation_dom_exception(scope, "Navigation was canceled", "AbortError"); if let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) { let host = unsafe { &mut *host_ptr }; @@ -360,7 +364,6 @@ pub(super) fn cancel_active_cross_document_navigation<'s>( { host.abort_signal(scope, signal, error); } - host.clear_pending_location_navigation(); } finish_navigation_error_events(scope, navigation, error, &href); let receiver = v8::undefined(scope).into(); 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 db17dd07f2..3045d9e480 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_traversal_execution.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_traversal_execution.rs @@ -533,5 +533,6 @@ fn queue_child_cross_document_traversal( target_url, seed, false, + None, ); } diff --git a/moli-renderer-v8/src/context_bootstrap/window_runtime/dialogs.rs b/moli-renderer-v8/src/context_bootstrap/window_runtime/dialogs.rs index 3bc1c62a29..21e756c566 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_runtime/dialogs.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_runtime/dialogs.rs @@ -107,6 +107,23 @@ pub(crate) fn window_stop_callback<'s>( args: v8::FunctionCallbackArguments<'s>, _rv: v8::ReturnValue<'_, v8::Value>, ) { + if !crate::context_bootstrap::is_window_receiver(scope, args.this()) { + webidl::throw_type_error(scope, "Window.stop called on incompatible receiver."); + return; + } + let owner = super::super::navigation_window::runtime_window_owner(scope, args.this()); + if super::super::navigation_window::navigation_unload_event_active(scope, owner) { + return; + } + if let Some(crate::native_bridge::OwnerDispatchScope::Child(handle)) = + super::super::navigation_window::runtime_window_dispatch_scope(scope, owner) + && let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) + && unsafe { &*host_ptr }.child_browsing_context_has_pending_cross_document_traversal(handle) + { + // Unlike an ordinary navigation, session history traversal is not + // canceled by Window.stop(). + return; + } inform_about_canceled_navigation_for_window(scope, args.this()); } diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commands.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commands.rs index 88a98cd990..787071861a 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commands.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_navigation/commands.rs @@ -105,6 +105,7 @@ impl JsContextHost { resolved_url: &str, entry_seed: NavigationHistoryEntrySeed, increments_joint_history: bool, + initiator_url: Option, ) -> bool { if !self.child_browsing_contexts.contains_key(&handle) { return false; @@ -112,6 +113,10 @@ impl JsContextHost { let Some(url) = Url::parse(resolved_url).ok() else { return false; }; + self.reject_replaced_service_worker_child_client_navigation( + handle, + "The navigation was canceled.".to_owned(), + ); if let Some(entry) = self.child_browsing_contexts.get_mut(&handle) { entry.replace_navigation_entry_seed_and_clear_pending_history_increment(entry_seed); if increments_joint_history { @@ -121,14 +126,15 @@ impl JsContextHost { if self .set_child_browsing_context_pending_navigation( handle, - ChildBrowsingContextBootstrap::Url(url), - None, + ChildBrowsingContextBootstrap::Url(url.clone()), + initiator_url, false, ) .is_none() { return false; } + self.register_reserved_service_worker_child_client_for_navigation(handle, &url); self.queue_child_browsing_context_navigation_commit(handle) } @@ -191,24 +197,49 @@ impl JsContextHost { self.queue_child_browsing_context_navigation_commit(handle) } - pub(crate) fn mark_child_browsing_context_top_level_history_increment( - &mut self, + pub(crate) fn child_browsing_context_has_pending_cross_document_traversal( + &self, handle: DomHandle, - ) { - if let Some(entry) = self.child_browsing_contexts.get_mut(&handle) { - entry.mark_pending_top_level_history_length_increment(); - } - } - - pub(crate) fn queue_child_browsing_context_reload_from_existing_seed( - &mut self, - handle: DomHandle, - resolved_url: &str, - initiator_url: Option, ) -> bool { - let Some(url) = Url::parse(resolved_url).ok() else { + let Some(entry) = self.child_browsing_contexts.get(&handle) else { return false; }; - self.queue_child_browsing_context_navigation_to_url(handle, &url, initiator_url) + if !entry.has_pending_navigation_or_document_load() { + return false; + } + let pending = entry.navigation_entry_seed(); + // The pending activation describes the destination, not the active + // Document's last navigation. A traversal must also change position. + pending.current_index != entry.committed_navigation_entry_seed().current_index + && pending + .activation + .as_ref() + .and_then(|activation| activation.navigation_type.as_deref()) + == Some("traverse") + } + + pub(crate) fn cancel_pending_child_browsing_context_navigation(&mut self, handle: DomHandle) { + let Some(entry) = self.child_browsing_contexts.get_mut(&handle) else { + return; + }; + if !entry.has_pending_navigation_or_document_load() { + return; + } + entry.clear_pending_navigation(); + entry.clear_pending_top_level_history_length_increment(); + entry.restore_navigation_entry_seed_from_committed(); + self.retire_current_child_navigation_commit_task(handle); + self.clear_pending_form_submission_child_target(handle); + self.reject_replaced_service_worker_child_client_navigation( + handle, + "The navigation was canceled.".to_owned(), + ); + if let Some(navigation_load) = self.current_child_navigation_load(handle) { + let _ = + self.finish_child_frame_navigation_without_load_dispatch(handle, navigation_load); + } + // Removing the request's owner also makes an already-arriving network + // completion stale; it must never install a Document after cancellation. + self.clear_pending_child_document_loads_for_handle(handle); } } diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs index 759062269d..781a433682 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs @@ -3467,6 +3467,275 @@ async fn same_document_navigation_precommit_added_handler_delays_finished() { assert_eq!(after_timeout, "handler|added|finished"); } +#[tokio::test] +async fn window_stop_cancels_child_location_navigation_without_committing_history() { + for action in [ + "child.location.search = '?blocked'", + "child.location.assign('/blocked')", + "child.location.replace('/blocked')", + "child.location.reload()", + "const result = child.navigation.navigate('/blocked'); result.committed.catch(() => {}); result.finished.catch(() => {})", + ] { + let server = StaticHttpServer::spawn(3).await; + let parent_url = server.url_for_host("window-stop.test", "/page.html"); + let loader = static_http_loader([server.resolve_entry("window-stop.test")]); + let mut vm = + new_storage_page_task_executor_test_vm_with_loader(parent_url.as_str(), &loader); + vm.eval( + r#" +globalThis.__stopFrame = document.createElement('iframe'); +globalThis.__stopLoads = 0; +__stopFrame.onload = () => ++__stopLoads; +__stopFrame.src = '/child.html'; +(document.body || document.documentElement || document).appendChild(__stopFrame); +"#, + ) + .expect("window.stop child setup should evaluate"); + advance_page_task_executor_until_eval_equals( + &mut vm, + &loader, + "String(__stopLoads)", + "1", + "initial child should load before the canceled navigation", + ) + .await; + + let snapshot = vm + .eval(&format!( + r#" +(() => {{ + globalThis.__stopSibling = document.createElement('iframe'); + globalThis.__stopSiblingLoaded = false; + __stopSibling.onload = () => {{ __stopSiblingLoaded = true; }}; + __stopSibling.src = '/sibling.html'; + (document.body || document.documentElement || document).appendChild(__stopSibling); + const child = __stopFrame.contentWindow; + child.history.replaceState({{ retained: true }}, '', '#original'); + const documentBefore = child.document; + const entryBefore = child.navigation.currentEntry; + const lengthBefore = child.history.length; + const hrefBefore = child.location.href; + const changes = []; + child.navigation.oncurrententrychange = () => changes.push('change'); + globalThis.__stopSnapshot = () => [ + child.document === documentBefore, + child.location.href === hrefBefore, + child.document.URL === hrefBefore, + child.navigation.currentEntry === entryBefore, + child.history.length === lengthBefore, + child.history.state?.retained === true, + changes.length, + __stopLoads + ].join('|'); + {action}; + const beforeStop = __stopSnapshot(); + child.stop(); + child.stop(); + return beforeStop + ';' + __stopSnapshot(); +}})() +"#, + )) + .expect("stopping a child navigation should evaluate"); + const UNCHANGED: &str = "true|true|true|true|true|true|0|1"; + assert_eq!(snapshot, format!("{UNCHANGED};{UNCHANGED}"), "{action}"); + + advance_page_task_executor_until_eval_equals( + &mut vm, + &loader, + "String(__stopSiblingLoaded)", + "true", + "a sibling should load while the stopped child remains unchanged", + ) + .await; + assert_eq!( + vm.eval("__stopSnapshot()") + .expect("stopped child snapshot should evaluate"), + UNCHANGED, + "{action} must not commit from a queued navigation task" + ); + + vm.eval("__stopFrame.contentWindow.location.assign('/after-stop.html')") + .expect("navigation after stop should schedule"); + advance_page_task_executor_until_eval_equals( + &mut vm, + &loader, + "String(__stopLoads)", + "2", + "stopping a navigation must not disable later navigations", + ) + .await; + assert_eq!( + vm.eval("__stopFrame.contentWindow.location.pathname") + .expect("resumed child URL should evaluate"), + "/after-stop.html" + ); + assert_eq!( + server.finish_targets().await, + ["/child.html", "/sibling.html", "/after-stop.html"], + "{action} must not start the canceled request" + ); + } +} + +#[test] +fn window_stop_in_child_does_not_cancel_the_top_level_navigation() { + let mut vm = new_storage_test_vm("https://window-stop.test/page.html"); + vm.eval( + r#" +globalThis.__stopFrame = document.createElement('iframe'); +__stopFrame.srcdoc = 'child'; +(document.body || document.documentElement || document).appendChild(__stopFrame); +"#, + ) + .expect("child setup should evaluate"); + vm.drain_pending_child_frame_work_for_test(); + vm.eval( + r#" +const child = __stopFrame.contentWindow; +const childResult = child.navigation.navigate('/child-next.html'); +childResult.committed.catch(() => {}); +childResult.finished.catch(() => {}); +const topResult = navigation.navigate('/top-next.html'); +topResult.committed.catch(() => {}); +topResult.finished.catch(() => {}); +child.stop(); +"#, + ) + .expect("stopping the child should leave the parent navigation alone"); + assert_eq!( + vm.take_pending_location_navigation_with_seed() + .expect("the top-level navigation must remain pending") + .url + .as_str(), + "https://window-stop.test/top-next.html" + ); +} + +#[tokio::test] +async fn window_stop_discards_in_flight_child_navigation_completions() { + let (child_url, request_rx, release_tx, server) = + spawn_gated_child_document_resource_server(200).await; + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); + let mut vm = new_storage_page_task_executor_test_vm_with_loader( + &child_url.replace("/child.html", "/page.html"), + &loader, + ); + vm.eval( + r#" +globalThis.__stopFrame = document.createElement('iframe'); +globalThis.__stopLoads = 0; +__stopFrame.onload = () => ++__stopLoads; +__stopFrame.srcdoc = 'original'; +(document.body || document.documentElement || document).appendChild(__stopFrame); +"#, + ) + .expect("in-flight navigation child setup should evaluate"); + advance_page_task_executor_until_eval_equals( + &mut vm, + &loader, + "String(__stopLoads)", + "1", + "srcdoc should finish before starting the blocked response", + ) + .await; + vm.eval(&format!( + "globalThis.__stopDocument = __stopFrame.contentDocument; __stopFrame.contentWindow.location.assign({child_url:?});" + )) + .expect("child navigation should queue"); + run_page_realm_prerequisite_then_expected_child_frame_semantic_turn( + &mut vm, + &loader, + ChildFrameSemanticTurnKind::NavigationCommit, + "navigation commit task should start the child request", + ) + .await; + request_rx.await.expect("the child request should arrive"); + assert_eq!( + vm.eval("__stopFrame.contentWindow.location.href") + .expect("in-flight child URL should evaluate"), + "about:srcdoc" + ); + vm.eval("__stopFrame.contentWindow.stop()") + .expect("in-flight child navigation should stop"); + release_tx.send(()).expect("release the canceled response"); + server.await.expect("the gated server should finish"); + wait_for_one_page_resource_completion_selected_task_executor_test_turn( + &mut vm, + &loader, + "late navigation completion should be discarded", + ) + .await; + assert_eq!( + vm.eval( + "[__stopFrame.contentDocument === __stopDocument, __stopFrame.contentWindow.location.href, __stopLoads].join('|')" + ) + .expect("late response must not replace the stopped child"), + "true|about:srcdoc|1" + ); +} + +#[tokio::test] +async fn window_stop_preserves_child_cross_document_history_traversal() { + let server = StaticHttpServer::spawn(3).await; + let parent_url = server.url_for_host("window-stop-traversal.test", "/page.html"); + let loader = static_http_loader([server.resolve_entry("window-stop-traversal.test")]); + let mut vm = new_storage_page_task_executor_test_vm_with_loader(parent_url.as_str(), &loader); + vm.eval( + r#" +globalThis.__stopFrame = document.createElement('iframe'); +globalThis.__stopLoads = 0; +__stopFrame.onload = () => ++__stopLoads; +__stopFrame.src = '/one.html'; +(document.body || document.documentElement || document).appendChild(__stopFrame); +"#, + ) + .expect("traversal child setup should evaluate"); + advance_page_task_executor_until_eval_equals( + &mut vm, + &loader, + "String(__stopLoads)", + "1", + "first child document should load", + ) + .await; + vm.eval("__stopFrame.contentWindow.location.assign('/two.html')") + .expect("second child navigation should schedule"); + advance_page_task_executor_until_eval_equals( + &mut vm, + &loader, + "String(__stopLoads)", + "2", + "second child document should load", + ) + .await; + vm.eval("__stopFrame.contentWindow.history.back()") + .expect("child history traversal should schedule"); + assert!( + vm.run_one_history_traversal_executor_turn(&loader) + .await + .expect("the history task should prepare the cross-document traversal") + ); + vm.eval("__stopFrame.contentWindow.stop()") + .expect("stop during traversal should evaluate"); + advance_page_task_executor_until_eval_equals( + &mut vm, + &loader, + "String(__stopLoads)", + "3", + "window.stop must not cancel a session history traversal", + ) + .await; + assert_eq!( + vm.eval("__stopFrame.contentWindow.location.pathname") + .expect("traversed URL should evaluate"), + "/one.html" + ); + assert_eq!( + server.finish_targets().await, + ["/one.html", "/two.html", "/one.html"] + ); +} + #[tokio::test] async fn window_stop_cancels_pending_precommit_before_commit() { let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs index a75aa7347e..cfc8606f1e 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs @@ -12687,13 +12687,21 @@ async fn child_sandbox_blocks_meta_refresh_when_it_is_created() { #[tokio::test] async fn child_meta_refresh_remains_scheduled_when_sandbox_is_added_later() { - let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); - let mut vm = new_storage_test_vm_with_loader("https://child-meta-refresh.test/", &loader); + let server = StaticHttpServer::spawn(1).await; + let parent_url = server.url_for_host("child-meta-refresh.test", "/"); + let loader = static_http_loader([server.resolve_entry("child-meta-refresh.test")]); + let mut vm = new_storage_page_task_executor_test_vm_with_loader(parent_url.as_str(), &loader); vm.eval( r#" (() => { const frame = document.createElement('iframe'); + globalThis.__childMetaRefreshLoads = 0; + frame.onload = () => { + if (++__childMetaRefreshLoads === 1) { + frame.setAttribute('sandbox', 'allow-same-origin'); + } + }; frame.srcdoc = '

source

'; (document.body || document.documentElement || document).appendChild(frame); return 'ready'; @@ -12701,21 +12709,22 @@ async fn child_meta_refresh_remains_scheduled_when_sandbox_is_added_later() { "#, ) .expect("unsandboxed child refresh setup should evaluate"); - vm.drain_pending_child_frame_work_for_test(); - vm.eval( - "document.querySelector('iframe').setAttribute('sandbox', 'allow-same-origin'); 'added'", + advance_page_task_executor_until_eval_equals( + &mut vm, + &loader, + "String(__childMetaRefreshLoads)", + "2", + "the scheduled refresh should load after the sandbox is added", ) - .expect("sandbox addition should evaluate"); - vm.advance_timers_until_deadline_for_test(&loader) - .await - .expect("scheduled child refresh should run"); + .await; assert_eq!( vm.eval("document.querySelector('iframe').contentWindow.location.href") .expect("child URL should remain observable"), - "https://child-meta-refresh.test/#allowed", + format!("{parent_url}#allowed"), "the sandbox policy is checked when the refresh is created, not again when it becomes due" ); + assert_eq!(server.finish_targets().await, ["/"]); } #[tokio::test]