From e86277ef0f74d65f81d7bcfdfb5c697b10032d71 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Thu, 10 Sep 2026 09:53:15 +0800 Subject: [PATCH] fix: unload descendant documents during frame navigation Snapshot retiring frame documents and finish their beforeunload phase before actual unload delivery. Preserve ancestor unload counters through descendant callbacks and check exact owners before dispatching further events. Retain visibility state on the native Document and dispatch trusted, bubbling visibilitychange events through the host event path. Use native unload counters to suppress navigation during visibility and ancestor callbacks; cancel each retiring window's timers after its unload. Eight Browser integration tests cover 50 scenarios. The 181-case WPT comparison gains two passing cases and seven passing subtests without regressions; update the passed ledger to 9,101 cases. Validation: cargo fmt --all; cargo clippy --workspace --all-targets --all-features -- -D warnings; cargo nextest run --no-fail-fast (17,910 passed, 13 skipped). Rebuilt CLI matches the tested WPT binary. --- .../wpt-cross-current/passed-cases.txt | 1 + moli-core/tests/document_open_unload.rs | 18 +- moli-core/tests/javascript_url_lifecycle.rs | 110 +++++++-- moli-dom/src/native/document.rs | 12 + moli-dom/src/native/host/document.rs | 19 ++ .../context_bootstrap/navigation_window.rs | 5 + .../context_host/child_documents/lifecycle.rs | 229 +++++++++++------- .../src/native_bridge/document.rs | 19 +- 8 files changed, 291 insertions(+), 122 deletions(-) diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt index 3976a2be71..6815a9a2bc 100644 --- a/moli-benchmark/wpt-cross-current/passed-cases.txt +++ b/moli-benchmark/wpt-cross-current/passed-cases.txt @@ -7321,6 +7321,7 @@ html/webappapis/dynamic-markup-insertion/opening-the-input-stream/document-open- html/webappapis/dynamic-markup-insertion/opening-the-input-stream/document.open-02.html html/webappapis/dynamic-markup-insertion/opening-the-input-stream/document.open-03.html html/webappapis/dynamic-markup-insertion/opening-the-input-stream/form-control-state.html +html/webappapis/dynamic-markup-insertion/opening-the-input-stream/ignore-opens-during-unload.window.js?moli-wpt-script=window html/webappapis/dynamic-markup-insertion/opening-the-input-stream/location-set-and-document-open.html html/webappapis/dynamic-markup-insertion/opening-the-input-stream/origin-check-in-document-open-basic.html html/webappapis/dynamic-markup-insertion/the-outerhtml-property/outerhtml-documentfragment.html diff --git a/moli-core/tests/document_open_unload.rs b/moli-core/tests/document_open_unload.rs index 66259a9e9a..463952c808 100644 --- a/moli-core/tests/document_open_unload.rs +++ b/moli-core/tests/document_open_unload.rs @@ -29,7 +29,7 @@ async fn stream_operation_during_unload( let listenerFrame = frame; let doc = frame.contentDocument; if (target === 'other') doc = (await frameIn(window)).contentDocument; - if (target === 'ancestor') {{ + if (target.endsWith('ancestor')) {{ const middle = await frameIn(frame.contentWindow); doc = middle.contentDocument; listenerFrame = await frameIn(middle.contentWindow); @@ -99,7 +99,7 @@ async fn stream_operation_during_unload( #[tokio::test(flavor = "multi_thread")] async fn document_stream_operations_have_no_side_effects_during_unload() -> Result<()> { - for event in ["beforeunload", "pagehide", "unload"] { + for event in ["beforeunload", "pagehide", "visibilitychange", "unload"] { for operation in [ "open", "prototype-open", @@ -132,9 +132,21 @@ async fn ancestor_unload_counter_covers_descendant_callbacks() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread")] +async fn ordinary_ancestor_unload_counter_covers_descendant_callbacks() -> Result<()> { + for event in ["beforeunload", "pagehide", "visibilitychange", "unload"] { + let result = stream_operation_during_unload(event, "open", "ordinary-ancestor").await?; + assert_eq!(result["sameRoot"], true, "{event}: {result}"); + assert_eq!(result["sameLength"], true); + assert_eq!(result["listenerCount"], 1); + assert_eq!(result["returnedDocument"], true); + } + Ok(()) +} + #[tokio::test(flavor = "multi_thread")] async fn unload_does_not_block_opening_another_document() -> Result<()> { - for event in ["beforeunload", "pagehide", "unload"] { + for event in ["beforeunload", "pagehide", "visibilitychange", "unload"] { let result = stream_operation_during_unload(event, "open", "other").await?; assert_eq!(result["sameRoot"], false, "{event}: {result}"); assert_eq!(result["listenerCount"], 0); diff --git a/moli-core/tests/javascript_url_lifecycle.rs b/moli-core/tests/javascript_url_lifecycle.rs index 17824219a6..5f06931a14 100644 --- a/moli-core/tests/javascript_url_lifecycle.rs +++ b/moli-core/tests/javascript_url_lifecycle.rs @@ -11,6 +11,9 @@ async fn child_navigation_lifecycle(via: &str, kind: &str, depth: usize) -> Resu let markup = format!( r#"