fix(navigation): reload idle initial-empty iframe documents

This commit is contained in:
ldm0
2026-10-02 02:20:52 +08:00
committed by Donough Liu
parent bac28518ff
commit dc5574676f
5 changed files with 68 additions and 14 deletions
@@ -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;
@@ -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
}
@@ -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;
}
@@ -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,
@@ -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");