diff --git a/moli-renderer-v8/src/context_bootstrap/location_navigation.rs b/moli-renderer-v8/src/context_bootstrap/location_navigation.rs index bd0ac60161..d3b5faaf47 100644 --- a/moli-renderer-v8/src/context_bootstrap/location_navigation.rs +++ b/moli-renderer-v8/src/context_bootstrap/location_navigation.rs @@ -260,7 +260,7 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>( if matches!(kind, LocationNavigationKind::Reload) && matches!( navigation_reload_admission(scope, owner), - NavigationReloadAdmission::NoCommittedHistoryItem + NavigationReloadAdmission::PendingInitialAttributeNavigation ) { return; diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_reload.rs b/moli-renderer-v8/src/context_bootstrap/navigation_reload.rs index 340d6190f3..045c3d0e79 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_reload.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_reload.rs @@ -1,15 +1,13 @@ use super::navigation_window::child_browsing_context_handle_for_runtime_owner; use super::*; -/// Whether the current Document owns a committed session-history item that -/// can be used as the source of a reload. -/// -/// An initial-empty child Document is installed synchronously before its first -/// real navigation and deliberately has no committed history item. Reloading -/// it must therefore leave any deferred iframe attribute navigation alone. +/// A deferred iframe attribute navigation still belongs to the initial empty +/// Document. Reloading that Document must leave the pending attribute load +/// alone; an ordinary initial about:blank iframe has an active history entry +/// and can be reloaded. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(super) enum NavigationReloadAdmission { - NoCommittedHistoryItem, + PendingInitialAttributeNavigation, Admitted, } @@ -23,8 +21,8 @@ pub(super) fn navigation_reload_admission<'s>( let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { return NavigationReloadAdmission::Admitted; }; - if unsafe { &*host_ptr }.child_current_document_is_initial_empty(handle) { - NavigationReloadAdmission::NoCommittedHistoryItem + if unsafe { &*host_ptr }.child_initial_empty_has_pending_attribute_navigation(handle) { + NavigationReloadAdmission::PendingInitialAttributeNavigation } else { NavigationReloadAdmission::Admitted } diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_traversal.rs b/moli-renderer-v8/src/context_bootstrap/navigation_traversal.rs index 8fa06a0e21..772028093d 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_traversal.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_traversal.rs @@ -254,10 +254,10 @@ pub(super) fn navigation_reload_callback<'s>( } if matches!( navigation_reload_admission(scope, owner), - NavigationReloadAdmission::NoCommittedHistoryItem + NavigationReloadAdmission::PendingInitialAttributeNavigation ) { - // Navigation API methods on an initial-empty Document return a - // correctly-shaped result whose promises intentionally never settle. + // Reloading the initial empty Document must not replace a deferred + // iframe attribute navigation. Its result promises remain pending. rv.set(navigation_pending_result(scope).into()); return; } diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window.rs index 2976259f5f..996a33d994 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window.rs @@ -836,6 +836,17 @@ impl JsContextHost { .is_some_and(crate::frame_owner_model::DocumentCreationKind::is_initial_empty) } + pub(crate) fn child_initial_empty_has_pending_attribute_navigation( + &self, + handle: DomHandle, + ) -> bool { + self.child_current_document_is_initial_empty(handle) + && self + .child_browsing_contexts + .get(&handle) + .is_some_and(|entry| entry.pending_attribute_bootstrap_commit()) + } + pub(crate) fn retire_child_frame_realm_materialization_request( &mut self, handle: DomHandle, diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/frame_navigation_and_history.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/frame_navigation_and_history.rs index de836d1b08..b0c9613655 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/frame_navigation_and_history.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/frame_navigation_and_history.rs @@ -2260,8 +2260,49 @@ __domainAccessFrame.src = globalThis.__replacementDomainChildUrl; vec!["/replacement.html"] ); } + #[test] -fn initial_empty_iframe_reload_uses_shared_no_history_admission() { +fn initial_empty_iframe_reload_methods_replace_document_and_dispatch_load() { + for reload in ["location.reload()", "history.go(0)", "navigation.reload()"] { + let mut vm = new_storage_test_vm("https://initial-empty-reload.test/page.html"); + + vm.exec( + &format!( + r#" +const frame = document.createElement('iframe'); +(document.body || document.documentElement || document).appendChild(frame); +globalThis.__initialReloadFrame = frame; +globalThis.__initialReloadDocument = frame.contentDocument; +globalThis.__initialReloadLoads = 0; +frame.onload = () => ++__initialReloadLoads; +frame.contentWindow.{reload}; +"# + ), + None, + ) + .expect("initial-empty iframe reload should evaluate"); + assert!( + vm.has_pending_child_navigation_commit_for_test(), + "reloading an ordinary initial about:blank iframe must queue a navigation" + ); + + vm.drain_pending_child_frame_work_for_test(); + assert_eq!( + vm.eval( + r#"[ + __initialReloadFrame.contentDocument !== __initialReloadDocument, + __initialReloadLoads, + __initialReloadFrame.contentWindow.location.href +].join('|')"#, + ) + .expect("initial-empty iframe reload result should evaluate"), + "true|1|about:blank" + ); + } +} + +#[test] +fn initial_empty_iframe_reload_preserves_pending_attribute_navigation_result_shape() { let mut vm = new_storage_test_vm("https://initial-empty-reload.test/page.html"); let setup = vm @@ -2269,6 +2310,9 @@ fn initial_empty_iframe_reload_uses_shared_no_history_admission() { r#" (() => { const frame = document.createElement('iframe'); + frame.loading = 'lazy'; + frame.hidden = true; + frame.src = '/pending-attribute-navigation.html'; (document.body || document.documentElement || document).appendChild(frame); const child = frame.contentWindow; const log = []; @@ -2337,6 +2381,7 @@ fn initial_empty_iframe_reload_uses_shared_no_history_admission() { "about:blank" ); } + #[test] fn non_initial_about_blank_iframe_remains_reloadable() { let mut vm = new_storage_test_vm("https://non-initial-blank-reload.test/page.html");