From 89dc9c80011501b46258c4dbb81dca7ddd5fc37b Mon Sep 17 00:00:00 2001 From: ldm0 Date: Tue, 8 Sep 2026 06:03:46 +0800 Subject: [PATCH] fix(navigation): defer child Location commits and honor window.stop Prepare pending child history seeds without mutating the active Location, history state, or NavigationHistoryEntry. Preserve initiator URLs and reserved service worker clients when queuing the deferred commit. Cancel pending child navigation tasks and invalidate in-flight response ownership without clearing another window navigation. Preserve cross-document history traversals and reject incompatible Window.stop receivers. Add regression coverage for five navigation entry points, retained document/history identity, sibling and parent isolation, late responses, resumed navigation, and traversal. Align integration assertions with committed URLs and test meta refresh through a real network load. Validation: cargo fmt --all; cargo clippy --workspace --all-targets --all-features -- -D warnings; cargo nextest run --no-fail-fast (17581 passed, 13 skipped). The 22 overlapping-navigation WPT cases improve from 12 to 16 passes with no lost passes; seven focused cases pass again. Move the four reproducibly fixed cases from failed to passed. An earlier full nextest run hit one empty-output assertion in cli_meta_refresh_shutdown_exits_without_renderer_abort; retained its log. Independent old/new binary probes did not reproduce it, and the complete rerun passed without changing or skipping that test. Artifacts: target/wpt-window-stop-20260908-XbyH6Q. Additional paired release validation: all 385 previously passing Navigation API cases remain passing out of 432. Two existing incomplete cases exchange timeout/harness-stalled classification; all other statuses, subtest counts and failure names are unchanged. Release SHA256: 11d9e1bf1faa64b07538c5f7ed93e7d04a6761d10d4d591433480a614df8a293. --- .../wpt-cross-current/failed-cases.txt | 3 - .../wpt-cross-current/passed-cases.txt | 3 + moli-core/tests/history_child.rs | 54 ++-- .../context_bootstrap/location_navigation.rs | 37 +-- .../navigation_cancellation.rs | 25 +- .../navigation_cross_document.rs | 2 +- .../context_bootstrap/navigation_result.rs | 5 +- .../navigation_traversal_execution.rs | 1 + .../window_runtime/dialogs.rs | 17 ++ .../child_frame_navigation/commands.rs | 65 +++-- .../script_vm/tests/browser_api/navigation.rs | 269 ++++++++++++++++++ .../tests/dom_elements/dom_surface.rs | 29 +- 12 files changed, 436 insertions(+), 74 deletions(-) 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]