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.
This commit is contained in:
ldm0
2026-09-13 17:02:09 +08:00
parent e26b13a7d4
commit 89dc9c8001
12 changed files with 436 additions and 74 deletions
@@ -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
@@ -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
+33 -21
View File
@@ -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(())
@@ -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;
@@ -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);
}
_ => {}
}
}
@@ -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())
@@ -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();
@@ -533,5 +533,6 @@ fn queue_child_cross_document_traversal(
target_url,
seed,
false,
None,
);
}
@@ -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());
}
@@ -105,6 +105,7 @@ impl JsContextHost {
resolved_url: &str,
entry_seed: NavigationHistoryEntrySeed,
increments_joint_history: bool,
initiator_url: Option<Url>,
) -> 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<Url>,
) -> 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);
}
}
@@ -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 = '<body>child</body>';
(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 = '<body>original</body>';
(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");
@@ -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 = '<meta http-equiv="refresh" content="0;url=#allowed"><p>source</p>';
(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]