fix(html): arm child meta refresh before load callbacks

Register the child document refresh before dispatching Window and owner-element
load callbacks. A callback can adopt the meta element into another iframe;
rescanning afterward lost the original refresh depending on frame load order.
The existing exact-document timer owner and navigation cancellation still apply.

Add deterministic source-first and destination-first iframe tests, including
adoption into an already loaded sandboxed document. The source-last case fails
before this change while the destination-last control passes. The old ordering
is present at least as far back as the earliest visible Public preview commit
27b5135cb6, rather than a newly introduced feature change.

Validation:
- cargo fmt --all
- cargo clippy --workspace --all-targets --all-features -- -D warnings
- cargo nextest run --no-fail-fast
This commit is contained in:
ldm0
2026-10-18 00:18:43 +08:00
parent 880e91a251
commit 7e79f9c154
2 changed files with 65 additions and 1 deletions
@@ -408,6 +408,9 @@ impl JsContextHost {
self.abort_and_requeue_child_load_delivery(action);
return ChildFrameLoadDeliveryPhaseResult::without_callback(None);
};
// Capture the original document's refresh before load callbacks can
// move or remove its meta element.
self.queue_child_meta_refresh_navigation_if_needed(scope, handle);
self.enter_child_browsing_context_host_load_dispatch(handle);
self.dispatch_child_window_event(scope, handle, "load", event);
self.leave_child_browsing_context_host_load_dispatch(handle);
@@ -450,7 +453,6 @@ impl JsContextHost {
self.trace_stale_child_load_delivery(action, "owner_element_load");
return ChildFrameLoadDeliveryPhaseResult::after_callback(None);
}
self.queue_child_meta_refresh_navigation_if_needed(scope, handle);
ChildFrameLoadDeliveryPhaseResult::after_callback(progress)
}
@@ -1358,6 +1358,68 @@ async fn detached_child_navigation_error_exposes_committed_entry_during_dispatch
"committed:true|error:true:true:AbortError|finished-rejected:AbortError||true"
);
}
async fn assert_child_meta_refresh_survives_adoption_during_load(source_loads_first: bool) {
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);
vm.eval(
r#"
globalThis.sourceLoads = 0;
globalThis.destinationLoads = 0;
const root = document.documentElement || document.appendChild(document.createElement('html'));
const source = document.createElement('iframe');
const destination = document.createElement('iframe');
destination.setAttribute('sandbox', 'allow-same-origin');
function adoptMetaWhenBothFramesHaveLoaded() {
if (sourceLoads === 1 && destinationLoads === 1) {
destination.contentDocument.body.appendChild(source.contentDocument.querySelector('meta'));
}
}
source.onload = () => { ++sourceLoads; adoptMetaWhenBothFramesHaveLoaded(); };
destination.onload = () => { ++destinationLoads; adoptMetaWhenBothFramesHaveLoaded(); };
source.srcdoc = '<meta http-equiv="refresh" content="0;url=about:srcdoc#refreshed"><p>source</p>';
destination.srcdoc = '<p>destination</p>';
'prepared'
"#,
)
.expect("meta adoption setup should evaluate");
let (first, second) = if source_loads_first {
("source", "destination")
} else {
("destination", "source")
};
vm.eval(&format!("root.appendChild({first}); 'queued'"))
.expect("first frame should attach");
vm.drain_pending_child_frame_work_for_test();
vm.eval(&format!("root.appendChild({second}); 'queued'"))
.expect("second frame should attach");
vm.drain_pending_child_frame_work_for_test();
assert_eq!(
vm.eval("String(destination.contentDocument.querySelectorAll('meta').length)")
.unwrap(),
"1",
"the load callback should adopt the refresh element into the sandboxed document",
);
vm.advance_timers_until_deadline_for_test(&loader)
.await
.expect("the original document's scheduled refresh should run");
assert_eq!(
vm.eval("JSON.stringify([source.contentWindow.location.href, destination.contentWindow.location.href, sourceLoads, destinationLoads])")
.unwrap(),
r#"["about:srcdoc#refreshed","about:srcdoc",1,1]"#,
"adoption must preserve the original refresh and must not refresh the sandboxed destination",
);
}
#[tokio::test]
async fn child_meta_refresh_survives_adoption_when_source_loads_last() {
assert_child_meta_refresh_survives_adoption_during_load(false).await;
}
#[tokio::test]
async fn child_meta_refresh_survives_adoption_when_destination_loads_last() {
assert_child_meta_refresh_survives_adoption_during_load(true).await;
}
#[tokio::test]
async fn child_meta_refresh_timer_is_canceled_when_frame_reloads() {
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");