mirror of
https://github.com/lexmount/moli.git
synced 2026-10-01 16:00:44 +00:00
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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -11,6 +11,9 @@ async fn child_navigation_lifecycle(via: &str, kind: &str, depth: usize) -> Resu
|
||||
let markup = format!(
|
||||
r#"<!doctype html><body><script>
|
||||
window.events = [];
|
||||
window.visibility = [];
|
||||
window.bubbledVisibility = [];
|
||||
window.syntheticDispatchCalled = false;
|
||||
window.staleTimerRan = false;
|
||||
window.unloadNavigationRan = false;
|
||||
window.finished = (async () => {{
|
||||
@@ -24,18 +27,31 @@ async fn child_navigation_lifecycle(via: &str, kind: &str, depth: usize) -> Resu
|
||||
const win = frame.contentWindow;
|
||||
for (const type of ['beforeunload', 'pagehide', 'unload'])
|
||||
win.addEventListener(type, () => events.push(label + ':' + type));
|
||||
win.document.addEventListener('visibilitychange', () =>
|
||||
events.push(label + ':visibilitychange'));
|
||||
win.addEventListener('unload', () => {{
|
||||
win.document.addEventListener('visibilitychange', event => {{
|
||||
events.push(label + ':visibilitychange');
|
||||
visibility.push({{label, hidden: win.document.hidden,
|
||||
state: win.document.visibilityState, trusted: event.isTrusted,
|
||||
bubbles: event.bubbles, cancelable: event.cancelable,
|
||||
documentTarget: event.target === win.document}});
|
||||
}});
|
||||
win.addEventListener('visibilitychange', () => bubbledVisibility.push(label));
|
||||
win.document.dispatchEvent = () => {{ syntheticDispatchCalled = true; }};
|
||||
const cleanup = () => {{
|
||||
win.setTimeout(() => staleTimerRan = true, 0);
|
||||
win.location.href = 'javascript:top.unloadNavigationRan = true; void 0';
|
||||
}});
|
||||
}};
|
||||
win.addEventListener('unload', cleanup);
|
||||
win.document.addEventListener('visibilitychange', cleanup);
|
||||
return frame;
|
||||
}}
|
||||
const frame = await makeFrame(window, 'target');
|
||||
let owner = frame.contentWindow;
|
||||
for (let i = 0; i < {depth}; ++i)
|
||||
owner = (await makeFrame(owner, 'descendant-' + i)).contentWindow;
|
||||
if ({depth} > 0) owner.addEventListener('unload', () => {{
|
||||
frame.contentWindow.location.href =
|
||||
'javascript:top.unloadNavigationRan = true; void 0';
|
||||
}});
|
||||
const unrelated = await makeFrame(window, 'unrelated');
|
||||
const oldDocument = frame.contentDocument;
|
||||
const unrelatedDocument = unrelated.contentDocument;
|
||||
@@ -60,8 +76,10 @@ async fn child_navigation_lifecycle(via: &str, kind: &str, depth: usize) -> Resu
|
||||
}}
|
||||
await done;
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
return {{events, staleTimerRan, unloadNavigationRan, loads,
|
||||
return {{events, visibility, bubbledVisibility, syntheticDispatchCalled,
|
||||
staleTimerRan, unloadNavigationRan, loads,
|
||||
sameDocument: frame.contentDocument === oldDocument,
|
||||
oldHidden: oldDocument.hidden, newHidden: frame.contentDocument.hidden,
|
||||
unrelatedUnchanged: unrelated.contentDocument === unrelatedDocument,
|
||||
text: frame.contentDocument.body.textContent,
|
||||
children: frame.contentWindow.length}};
|
||||
@@ -83,6 +101,11 @@ async fn child_navigation_lifecycle(via: &str, kind: &str, depth: usize) -> Resu
|
||||
server.shutdown().await;
|
||||
assert_eq!(result["unrelatedUnchanged"], true, "{via}/{kind}: {result}");
|
||||
assert_eq!(result["staleTimerRan"], false, "{via}/{kind}: {result}");
|
||||
assert_eq!(
|
||||
result["syntheticDispatchCalled"], false,
|
||||
"{via}/{kind}: {result}"
|
||||
);
|
||||
assert_eq!(result["newHidden"], false, "{via}/{kind}: {result}");
|
||||
assert_eq!(
|
||||
result["unloadNavigationRan"], false,
|
||||
"{via}/{kind}: {result}"
|
||||
@@ -137,28 +160,63 @@ async fn child_javascript_url_non_string_preserves_document_and_descendants() ->
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn ordinary_child_navigation_still_dispatches_beforeunload() -> Result<()> {
|
||||
async fn ordinary_child_navigation_unloads_descendants_and_updates_visibility() -> Result<()> {
|
||||
for via in ["location", "src", "anchor"] {
|
||||
let result = child_navigation_lifecycle(via, "network", 0).await?;
|
||||
assert_eq!(result["sameDocument"], false);
|
||||
assert_eq!(result["loads"], 1);
|
||||
assert_eq!(result["text"], "network");
|
||||
let events: Vec<_> = result["events"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|event| *event != "target:visibilitychange")
|
||||
.cloned()
|
||||
.collect();
|
||||
assert_eq!(
|
||||
events,
|
||||
vec![
|
||||
json!("target:beforeunload"),
|
||||
json!("target:pagehide"),
|
||||
json!("target:unload")
|
||||
],
|
||||
"{via}: {result}"
|
||||
);
|
||||
for depth in [0, 2] {
|
||||
let result = child_navigation_lifecycle(via, "network", depth).await?;
|
||||
assert_eq!(result["sameDocument"], false);
|
||||
assert_eq!(result["loads"], 1);
|
||||
assert_eq!(result["text"], "network");
|
||||
assert_eq!(result["oldHidden"], true);
|
||||
let events = result["events"].as_array().unwrap();
|
||||
let labels: Vec<_> = std::iter::once("target".to_owned())
|
||||
.chain((0..depth).map(|index| format!("descendant-{index}")))
|
||||
.collect();
|
||||
// Cancellation checks for every document precede actual unloads.
|
||||
assert!(
|
||||
events
|
||||
.iter()
|
||||
.take(labels.len())
|
||||
.all(|event| event.as_str().unwrap().ends_with(":beforeunload")),
|
||||
"{via}/{depth}: {result}"
|
||||
);
|
||||
for label in &labels {
|
||||
let actual: Vec<_> = events
|
||||
.iter()
|
||||
.filter(|event| event.as_str().unwrap().starts_with(&format!("{label}:")))
|
||||
.cloned()
|
||||
.collect();
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![
|
||||
json!(format!("{label}:beforeunload")),
|
||||
json!(format!("{label}:pagehide")),
|
||||
json!(format!("{label}:visibilitychange")),
|
||||
json!(format!("{label}:unload"))
|
||||
],
|
||||
"{via}/{depth}: {result}"
|
||||
);
|
||||
assert!(
|
||||
result["visibility"].as_array().unwrap().contains(&json!({
|
||||
"label": label, "hidden": true, "state": "hidden", "trusted": true,
|
||||
"bubbles": true, "cancelable": false, "documentTarget": true
|
||||
})),
|
||||
"{via}/{depth}: {result}"
|
||||
);
|
||||
assert!(
|
||||
result["bubbledVisibility"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.contains(&json!(label))
|
||||
);
|
||||
}
|
||||
assert_eq!(events.len(), 4 * labels.len(), "{via}/{depth}: {result}");
|
||||
assert_eq!(result["visibility"].as_array().unwrap().len(), labels.len());
|
||||
assert_eq!(
|
||||
result["bubbledVisibility"].as_array().unwrap().len(),
|
||||
labels.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -49,6 +49,8 @@ pub struct Document {
|
||||
url: Url,
|
||||
content_type: Box<str>,
|
||||
ready_state: DocumentReadyState,
|
||||
// Retained with the Document when its Window is replaced during navigation.
|
||||
visibility_hidden: bool,
|
||||
quirks_mode: QuirksMode,
|
||||
kind: DocumentKind,
|
||||
// The document's HTML scripting flag. Browsing-context policy can still
|
||||
@@ -77,6 +79,7 @@ impl Document {
|
||||
url,
|
||||
content_type: "text/html".into(),
|
||||
ready_state: DocumentReadyState::Complete,
|
||||
visibility_hidden: false,
|
||||
quirks_mode: QuirksMode::NoQuirks,
|
||||
kind: DocumentKind::Html,
|
||||
scripting_enabled,
|
||||
@@ -93,6 +96,7 @@ impl Document {
|
||||
url,
|
||||
content_type: "application/xml".into(),
|
||||
ready_state: DocumentReadyState::Complete,
|
||||
visibility_hidden: false,
|
||||
quirks_mode: QuirksMode::NoQuirks,
|
||||
kind: DocumentKind::Xml,
|
||||
scripting_enabled: true,
|
||||
@@ -119,6 +123,10 @@ impl Document {
|
||||
self.ready_state
|
||||
}
|
||||
|
||||
pub fn visibility_hidden(&self) -> bool {
|
||||
self.visibility_hidden
|
||||
}
|
||||
|
||||
pub fn default_language(&self) -> Option<&str> {
|
||||
self.default_language.as_deref()
|
||||
}
|
||||
@@ -204,6 +212,10 @@ impl Document {
|
||||
self.ready_state = ready_state;
|
||||
}
|
||||
|
||||
pub fn set_visibility_hidden(&mut self, hidden: bool) {
|
||||
self.visibility_hidden = hidden;
|
||||
}
|
||||
|
||||
pub fn set_default_language(&mut self, language: Option<String>) {
|
||||
self.default_language = language.map(String::into_boxed_str);
|
||||
}
|
||||
|
||||
@@ -1379,6 +1379,25 @@ impl DomHost {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn set_document_visibility_hidden_for_handle(
|
||||
&mut self,
|
||||
document_handle: DomHandle,
|
||||
hidden: bool,
|
||||
) -> bool {
|
||||
let Some(document) = self
|
||||
.node_mut(document_handle)
|
||||
.and_then(|node| node.data_mut().as_document_mut())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
if document.visibility_hidden() == hidden {
|
||||
return false;
|
||||
}
|
||||
document.set_visibility_hidden(hidden);
|
||||
self.record_mutation(MutationScope::LocalState);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn child_nodes(&self, handle: DomHandle) -> Option<Vec<DomHandle>> {
|
||||
self.node(handle)?;
|
||||
Some(self.child_handles(handle).collect())
|
||||
|
||||
@@ -190,6 +190,11 @@ pub(super) fn navigation_unload_event_active<'s>(
|
||||
owner: v8::Local<'s, v8::Object>,
|
||||
) -> bool {
|
||||
object_bool_property(scope, owner, WINDOW_UNLOAD_EVENT_ACTIVE_SLOT).unwrap_or(false)
|
||||
|| context_host_ptr_from_global_bridge(scope).is_some_and(|host_ptr| {
|
||||
let host = unsafe { &*host_ptr };
|
||||
super::window_accessors::window_document_handle(scope, owner, host)
|
||||
.is_some_and(|document| host.has_document_unload_counter(document))
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn set_navigation_unload_event_active<'s>(
|
||||
|
||||
@@ -7,9 +7,8 @@ enum ChildDocumentInteractiveScriptDisposition {
|
||||
}
|
||||
use crate::{
|
||||
context_bootstrap::{
|
||||
construct_original_event, dispatch_beforeunload_for_runtime_owner,
|
||||
dispatch_pagehide_for_runtime_owner, dispatch_unload_for_runtime_owner,
|
||||
record_performance_dom_content_loaded_event_end,
|
||||
dispatch_beforeunload_for_runtime_owner, dispatch_pagehide_for_runtime_owner,
|
||||
dispatch_unload_for_runtime_owner, record_performance_dom_content_loaded_event_end,
|
||||
record_performance_dom_content_loaded_event_start,
|
||||
},
|
||||
detached_event_target::dispatch_detached_simple_event,
|
||||
@@ -20,13 +19,10 @@ use crate::{
|
||||
FrameDocumentDescendantLoadParent, FrameDocumentInteractiveLifecycleAction,
|
||||
FrameDocumentLifecycleAction, FrameDocumentLifecycleTaskEffect,
|
||||
FrameDocumentLoadDeliveryTask, FrameDocumentNavigationLoadBinding, FrameDocumentTaskOwner,
|
||||
FrameRealmId,
|
||||
FrameDocumentUnloadLifecycleAction, FrameRealmId,
|
||||
},
|
||||
native_bridge::document::DETACHED_STATE_SLOT,
|
||||
util::{
|
||||
call_object_method, context_host_ptr_from_global_bridge, get_private_object, v8_string,
|
||||
v8str,
|
||||
},
|
||||
util::{context_host_ptr_from_global_bridge, get_private_object, v8_string, v8str},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
@@ -875,45 +871,88 @@ impl JsContextHost {
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
handle: DomHandle,
|
||||
) -> bool {
|
||||
let Some(window) = self.existing_child_browsing_context_window_wrapper(scope, handle)
|
||||
else {
|
||||
return false;
|
||||
let documents = self.child_document_unload_tree_snapshot(handle);
|
||||
let mut unload_guards = Vec::new();
|
||||
let mut actions = Vec::new();
|
||||
// Every descendant gets its cancellation check before actual unload
|
||||
// begins. Keep ancestor counters active across descendant callbacks.
|
||||
for (handle, document, parent_document) in documents {
|
||||
while unload_guards
|
||||
.last()
|
||||
.is_some_and(|(document, _)| Some(*document) != parent_document)
|
||||
{
|
||||
unload_guards.pop();
|
||||
}
|
||||
if self.child_browsing_context_document_handle(handle) != Some(document) {
|
||||
continue;
|
||||
}
|
||||
let Some(window) = self.existing_child_browsing_context_window_wrapper(scope, handle)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if self
|
||||
.child_browsing_context_document_wrapper(scope, handle)
|
||||
.is_none()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let Some(action) = self
|
||||
.frame_owner_store
|
||||
.begin_current_child_document_unload(handle)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
unload_guards.push((document, self.enter_document_unload(document)));
|
||||
actions.push((document, parent_document, action));
|
||||
dispatch_beforeunload_for_runtime_owner(scope, window);
|
||||
}
|
||||
unload_guards.clear();
|
||||
let dispatched = !actions.is_empty();
|
||||
for (document, parent_document, action) in actions {
|
||||
while unload_guards
|
||||
.last()
|
||||
.is_some_and(|(document, _)| Some(*document) != parent_document)
|
||||
{
|
||||
unload_guards.pop();
|
||||
}
|
||||
if self
|
||||
.frame_owner_store
|
||||
.child_document_task_owner_is_current(action.child_handle(), action.owner())
|
||||
{
|
||||
unload_guards.push((document, self.enter_document_unload(document)));
|
||||
Self::dispatch_child_document_unload_action(scope, self, action);
|
||||
}
|
||||
}
|
||||
dispatched
|
||||
}
|
||||
|
||||
fn child_document_unload_tree_snapshot(
|
||||
&self,
|
||||
handle: DomHandle,
|
||||
) -> Vec<(DomHandle, DomHandle, Option<DomHandle>)> {
|
||||
let Some(document) = self.child_browsing_context_document_handle(handle) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(_document) = self.child_browsing_context_document_wrapper(scope, handle) else {
|
||||
return false;
|
||||
};
|
||||
let Some(action) = self
|
||||
.frame_owner_store
|
||||
.begin_current_child_document_unload(handle)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
tracing::debug!(
|
||||
?handle,
|
||||
owner = ?action.owner(),
|
||||
"dispatching document-owned child unload lifecycle"
|
||||
let mut handles = vec![handle];
|
||||
self.collect_child_browsing_context_handles_in_document_order_from_document(
|
||||
document,
|
||||
&mut handles,
|
||||
);
|
||||
let execution_context_owner = crate::native_bridge::WindowExecutionContextOwner::Frame(
|
||||
action.owner().local_window_id,
|
||||
);
|
||||
let _document_unload = self
|
||||
.child_browsing_context_document_handle(handle)
|
||||
.map(|document| self.enter_document_unload(document));
|
||||
dispatch_beforeunload_for_runtime_owner(scope, window);
|
||||
dispatch_pagehide_for_runtime_owner(scope, window);
|
||||
dispatch_unload_for_runtime_owner(scope, window);
|
||||
let finished = self
|
||||
.frame_owner_store
|
||||
.finish_current_child_document_unload(action);
|
||||
tracing::debug!(
|
||||
?handle,
|
||||
owner = ?action.owner(),
|
||||
finished,
|
||||
"finished document-owned child unload lifecycle"
|
||||
);
|
||||
unsafe { &mut *self.runtime }
|
||||
.cancel_window_execution_context_timers(execution_context_owner);
|
||||
true
|
||||
// Snapshot the documents before any unload handler can remove or
|
||||
// replace a descendant. A new document must not inherit this unload.
|
||||
handles
|
||||
.into_iter()
|
||||
.filter_map(|handle| {
|
||||
self.child_browsing_context_document_handle(handle)
|
||||
.map(|document| {
|
||||
(
|
||||
handle,
|
||||
document,
|
||||
self.dom_host().owner_document_handle(handle),
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(in crate::native_bridge::context_host) fn dispatch_child_javascript_url_unload_lifecycle(
|
||||
@@ -921,29 +960,7 @@ impl JsContextHost {
|
||||
host_ptr: *mut Self,
|
||||
handle: DomHandle,
|
||||
) {
|
||||
let Some(document) = unsafe { &*host_ptr }.child_browsing_context_document_handle(handle) else {
|
||||
return;
|
||||
};
|
||||
let mut handles = vec![handle];
|
||||
unsafe { &*host_ptr }.collect_child_browsing_context_handles_in_document_order_from_document(
|
||||
document,
|
||||
&mut handles,
|
||||
);
|
||||
// Snapshot the documents before any unload handler can remove or
|
||||
// replace a descendant. A new document must not inherit this unload.
|
||||
let documents: Vec<_> = handles
|
||||
.into_iter()
|
||||
.filter_map(|handle| {
|
||||
unsafe { &*host_ptr }.child_browsing_context_document_handle(handle)
|
||||
.map(|document| {
|
||||
(
|
||||
handle,
|
||||
document,
|
||||
unsafe { &*host_ptr }.dom_host().owner_document_handle(handle),
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let documents = unsafe { &mut *host_ptr }.child_document_unload_tree_snapshot(handle);
|
||||
// Keep an ancestor's counter active while its descendants unload,
|
||||
// and release a completed sibling before entering the next subtree.
|
||||
let mut unload_guards = Vec::new();
|
||||
@@ -954,8 +971,8 @@ impl JsContextHost {
|
||||
{
|
||||
unload_guards.pop();
|
||||
}
|
||||
if unsafe { &*host_ptr }.child_browsing_context_document_handle(handle) == Some(document) {
|
||||
unload_guards.push((document, unsafe { &*host_ptr }.enter_document_unload(document)));
|
||||
if unsafe { &mut *host_ptr }.child_browsing_context_document_handle(handle) == Some(document) {
|
||||
unload_guards.push((document, unsafe { &mut *host_ptr }.enter_document_unload(document)));
|
||||
Self::dispatch_child_document_unload_without_beforeunload(scope, host_ptr, handle);
|
||||
}
|
||||
}
|
||||
@@ -969,38 +986,80 @@ impl JsContextHost {
|
||||
host_ptr: *mut Self,
|
||||
handle: DomHandle,
|
||||
) {
|
||||
let Some(window) =
|
||||
unsafe { &mut *host_ptr }.existing_child_browsing_context_window_wrapper(scope, handle)
|
||||
else {
|
||||
if unsafe { &mut *host_ptr }
|
||||
.existing_child_browsing_context_window_wrapper(scope, handle)
|
||||
.is_none()
|
||||
|| unsafe { &mut *host_ptr }
|
||||
.child_browsing_context_document_wrapper(scope, handle)
|
||||
.is_none()
|
||||
{
|
||||
return;
|
||||
};
|
||||
let Some(document) =
|
||||
unsafe { &mut *host_ptr }.child_browsing_context_document_wrapper(scope, handle)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
}
|
||||
let Some(action) = unsafe { &mut *host_ptr }
|
||||
.frame_owner_store
|
||||
.begin_current_child_document_unload(handle)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
Self::dispatch_child_document_unload_action(scope, host_ptr, action);
|
||||
}
|
||||
|
||||
fn dispatch_child_document_unload_action(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut Self,
|
||||
action: FrameDocumentUnloadLifecycleAction,
|
||||
) {
|
||||
let handle = action.child_handle();
|
||||
if !unsafe { &mut *host_ptr }
|
||||
.frame_owner_store
|
||||
.child_document_task_owner_is_current(handle, action.owner())
|
||||
{
|
||||
return;
|
||||
}
|
||||
let Some(window) = unsafe { &mut *host_ptr }.existing_child_browsing_context_window_wrapper(scope, handle)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(document) = unsafe { &mut *host_ptr }.child_browsing_context_document_wrapper(scope, handle) else {
|
||||
return;
|
||||
};
|
||||
let execution_context_owner = crate::native_bridge::WindowExecutionContextOwner::Frame(
|
||||
action.owner().local_window_id,
|
||||
);
|
||||
|
||||
let _document_unload = unsafe { &*host_ptr }
|
||||
let _document_unload = unsafe { &mut *host_ptr }
|
||||
.child_browsing_context_document_handle(handle)
|
||||
.map(|document| unsafe { &*host_ptr }.enter_document_unload(document));
|
||||
.map(|document| unsafe { &mut *host_ptr }.enter_document_unload(document));
|
||||
dispatch_pagehide_for_runtime_owner(scope, window);
|
||||
if let Some(event) = construct_original_event(scope, "visibilitychange") {
|
||||
let _ = call_object_method(scope, document, "dispatchEvent", &[event.into()]);
|
||||
if let Some(document_handle) = unsafe { &mut *host_ptr }.child_browsing_context_document_handle(handle)
|
||||
&& unsafe { &mut *host_ptr }
|
||||
.frame_owner_store
|
||||
.child_document_task_owner_is_current(handle, action.owner())
|
||||
&& unsafe { &mut *host_ptr }
|
||||
.dom_host_mut()
|
||||
.set_document_visibility_hidden_for_handle(document_handle, true)
|
||||
{
|
||||
unsafe { &mut *host_ptr }.dispatch_child_document_event_for_owner(
|
||||
scope,
|
||||
handle,
|
||||
action.owner(),
|
||||
document,
|
||||
"visibilitychange",
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
if unsafe { &mut *host_ptr }
|
||||
.frame_owner_store
|
||||
.child_document_task_owner_is_current(handle, action.owner())
|
||||
{
|
||||
dispatch_unload_for_runtime_owner(scope, window);
|
||||
}
|
||||
dispatch_unload_for_runtime_owner(scope, window);
|
||||
let _ = unsafe { &mut *host_ptr }
|
||||
.frame_owner_store
|
||||
.finish_current_child_document_unload(action);
|
||||
unsafe { &mut *host_ptr }.cancel_window_execution_context_timers(execution_context_owner);
|
||||
unsafe { &mut *unsafe { &mut *host_ptr }.runtime }
|
||||
.cancel_window_execution_context_timers(execution_context_owner);
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_document_open_descendant_frame_unload_lifecycle(
|
||||
|
||||
@@ -1415,6 +1415,13 @@ fn document_has_browsing_context(runtime: &JsContextHost, handle: DomHandle) ->
|
||||
.is_some()
|
||||
}
|
||||
|
||||
fn document_is_hidden(runtime: &JsContextHost, handle: DomHandle) -> bool {
|
||||
!runtime.document_activity().visible
|
||||
|| runtime.dom_host().node(handle).and_then(Node::as_document)
|
||||
.is_some_and(|document| document.visibility_hidden())
|
||||
|| !document_has_browsing_context(runtime, handle)
|
||||
}
|
||||
|
||||
fn document_hidden_getter_function<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
@@ -1425,9 +1432,7 @@ fn document_hidden_getter_function<'s>(
|
||||
rv.set_undefined();
|
||||
return;
|
||||
};
|
||||
let visible = unsafe { &*runtime_ptr }.document_activity().visible
|
||||
&& document_has_browsing_context(unsafe { &*runtime_ptr }, handle);
|
||||
rv.set_bool(!visible);
|
||||
rv.set_bool(document_is_hidden(unsafe { &*runtime_ptr }, handle));
|
||||
}
|
||||
|
||||
fn document_visibility_state_getter_function<'s>(
|
||||
@@ -1440,12 +1445,10 @@ fn document_visibility_state_getter_function<'s>(
|
||||
rv.set_undefined();
|
||||
return;
|
||||
};
|
||||
let state = if unsafe { &*runtime_ptr }.document_activity().visible
|
||||
&& document_has_browsing_context(unsafe { &*runtime_ptr }, handle)
|
||||
{
|
||||
"visible"
|
||||
} else {
|
||||
let state = if document_is_hidden(unsafe { &*runtime_ptr }, handle) {
|
||||
"hidden"
|
||||
} else {
|
||||
"visible"
|
||||
};
|
||||
set_document_string_return_value(scope, &mut rv, state);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user