From 7e79f9c154ad8915b5708dfc88d1d3c59fa65c69 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sun, 18 Oct 2026 00:18:43 +0800 Subject: [PATCH] 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 27b5135cb6c, 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 --- .../native_bridge/context_host/host_loads.rs | 4 +- .../extracted/frame_navigation_and_history.rs | 62 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/moli-renderer-v8/src/native_bridge/context_host/host_loads.rs b/moli-renderer-v8/src/native_bridge/context_host/host_loads.rs index 283a349531..6cc269a2bf 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/host_loads.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/host_loads.rs @@ -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) } 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 dd68ece10c..e8fd18bee9 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 @@ -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 = '

source

'; +destination.srcdoc = '

destination

'; +'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");