mirror of
https://github.com/lexmount/moli.git
synced 2026-10-05 16:00:54 +00:00
fix(navigation): share non-cancelable child unload lifecycle
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
use anyhow::Result;
|
||||
use moli_core::runtime::{Browser, BrowserConfig};
|
||||
use moli_test_support::FixtureServer;
|
||||
use serde_json::{Value, json};
|
||||
use tokio::time::Duration;
|
||||
use url::Url;
|
||||
|
||||
async fn child_navigation_lifecycle(via: &str, kind: &str, depth: usize) -> Result<Value> {
|
||||
let server = FixtureServer::spawn().await?;
|
||||
let browser = Browser::new(BrowserConfig::default())?;
|
||||
let markup = format!(
|
||||
r#"<!doctype html><body><script>
|
||||
window.events = [];
|
||||
window.staleTimerRan = false;
|
||||
window.unloadNavigationRan = false;
|
||||
window.finished = (async () => {{
|
||||
async function makeFrame(owner, label) {{
|
||||
const frame = owner.document.createElement('iframe');
|
||||
const loaded = new Promise(resolve => frame.onload = resolve);
|
||||
frame.src = '/compat/child-dynamic-markup-document?markup=' +
|
||||
encodeURIComponent('<!doctype html><body>original');
|
||||
owner.document.body.append(frame);
|
||||
await loaded;
|
||||
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.setTimeout(() => staleTimerRan = true, 0);
|
||||
win.location.href = 'javascript:top.unloadNavigationRan = true; void 0';
|
||||
}});
|
||||
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;
|
||||
const unrelated = await makeFrame(window, 'unrelated');
|
||||
const oldDocument = frame.contentDocument;
|
||||
const unrelatedDocument = unrelated.contentDocument;
|
||||
let resolveDone;
|
||||
const done = new Promise(resolve => resolveDone = resolve);
|
||||
let loads = 0;
|
||||
frame.onload = () => {{ loads++; resolveDone(); }};
|
||||
window.nonStringDone = () => setTimeout(resolveDone, 0);
|
||||
const kind = {kind:?};
|
||||
const url = kind === 'string' ? 'javascript:"<body>replacement"' :
|
||||
kind === 'undefined' ? 'javascript:top.nonStringDone(); void 0' :
|
||||
'/compat/child-dynamic-markup-document?markup=' +
|
||||
encodeURIComponent('<!doctype html><body>network');
|
||||
const via = {via:?};
|
||||
if (via === 'location') frame.contentWindow.location.href = url;
|
||||
else if (via === 'src') frame.src = url;
|
||||
else {{
|
||||
const anchor = frame.contentDocument.createElement('a');
|
||||
anchor.href = url;
|
||||
frame.contentDocument.body.append(anchor);
|
||||
anchor.click();
|
||||
}}
|
||||
await done;
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
return {{events, staleTimerRan, unloadNavigationRan, loads,
|
||||
sameDocument: frame.contentDocument === oldDocument,
|
||||
unrelatedUnchanged: unrelated.contentDocument === unrelatedDocument,
|
||||
text: frame.contentDocument.body.textContent,
|
||||
children: frame.contentWindow.length}};
|
||||
}})();
|
||||
</script>"#
|
||||
);
|
||||
let mut url = Url::parse(&server.url("/compat/child-dynamic-markup-document"))?;
|
||||
url.query_pairs_mut().append_pair("markup", &markup);
|
||||
let result = tokio::time::timeout(Duration::from_secs(10), async {
|
||||
let mut page = browser.fetch(url.as_str()).await?;
|
||||
page.evaluate_runtime_expression_with_await_async(
|
||||
"finished.then(value => JSON.stringify(value))",
|
||||
true,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await??;
|
||||
let result: Value = serde_json::from_str(result["value"].as_str().unwrap())?;
|
||||
server.shutdown().await;
|
||||
assert_eq!(result["unrelatedUnchanged"], true, "{via}/{kind}: {result}");
|
||||
assert_eq!(result["staleTimerRan"], false, "{via}/{kind}: {result}");
|
||||
assert_eq!(
|
||||
result["unloadNavigationRan"], false,
|
||||
"{via}/{kind}: {result}"
|
||||
);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn child_javascript_url_string_unloads_without_beforeunload() -> Result<()> {
|
||||
for via in ["location", "src", "anchor"] {
|
||||
for depth in [0, 2] {
|
||||
let result = child_navigation_lifecycle(via, "string", depth).await?;
|
||||
assert_eq!(result["sameDocument"], false);
|
||||
assert_eq!(result["loads"], 1);
|
||||
assert_eq!(result["text"], "replacement");
|
||||
assert_eq!(result["children"], 0);
|
||||
let events = result["events"].as_array().unwrap();
|
||||
let labels = std::iter::once("target".to_owned())
|
||||
.chain((0..depth).map(|index| format!("descendant-{index}")));
|
||||
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}:pagehide")),
|
||||
json!(format!("{label}:visibilitychange")),
|
||||
json!(format!("{label}:unload")),
|
||||
],
|
||||
"{via}/{depth}: {result}"
|
||||
);
|
||||
}
|
||||
assert_eq!(events.len(), 3 * (depth + 1), "{via}/{depth}: {result}");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn child_javascript_url_non_string_preserves_document_and_descendants() -> Result<()> {
|
||||
for via in ["location", "src", "anchor"] {
|
||||
let result = child_navigation_lifecycle(via, "undefined", 2).await?;
|
||||
assert_eq!(result["sameDocument"], true);
|
||||
assert_eq!(result["loads"], 0);
|
||||
assert_eq!(result["events"], json!([]));
|
||||
assert_eq!(result["children"], 1);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn ordinary_child_navigation_still_dispatches_beforeunload() -> 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}"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -867,14 +867,44 @@ impl JsContextHost {
|
||||
true
|
||||
}
|
||||
|
||||
/// Dispatches the unload sequence used when `Document::open()` removes a
|
||||
/// descendant frame.
|
||||
///
|
||||
/// This is intentionally distinct from navigation teardown: Chromium's
|
||||
/// document-open steps do not prompt the child with `beforeunload`, and
|
||||
/// dispatch pagehide/visibilitychange before unload while the parent
|
||||
/// document's listeners are still installed.
|
||||
fn dispatch_child_browsing_context_document_open_unload_lifecycle_if_needed(
|
||||
pub(in crate::native_bridge::context_host) fn dispatch_child_javascript_url_unload_lifecycle(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
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))
|
||||
})
|
||||
.collect();
|
||||
for (handle, document) in documents {
|
||||
if unsafe { &*host_ptr }.child_browsing_context_document_handle(handle)
|
||||
== Some(document)
|
||||
{
|
||||
Self::dispatch_child_document_unload_without_beforeunload(scope, host_ptr, handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// JavaScript URL replacement and removal by `Document::open()` unload
|
||||
/// documents without checking whether unloading is canceled. They still
|
||||
/// dispatch the actual unload lifecycle and cancel the old window's timers.
|
||||
fn dispatch_child_document_unload_without_beforeunload(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut Self,
|
||||
handle: DomHandle,
|
||||
@@ -924,9 +954,7 @@ impl JsContextHost {
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Self::dispatch_child_browsing_context_document_open_unload_lifecycle_if_needed(
|
||||
scope, host_ptr, handle,
|
||||
);
|
||||
Self::dispatch_child_document_unload_without_beforeunload(scope, host_ptr, handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -669,7 +669,7 @@ impl JsContextHost {
|
||||
self.child_document_credentialless_storage_nonce(document_credentialless);
|
||||
|
||||
self.clear_pending_child_document_loads_for_handle(handle);
|
||||
self.dispatch_child_browsing_context_unload_lifecycle_if_needed(scope, handle);
|
||||
Self::dispatch_child_javascript_url_unload_lifecycle(scope, self, handle);
|
||||
if !self.child_document_window_commit_preflight_is_current(handle, &window_commit_preflight)
|
||||
{
|
||||
let _ = self.finish_child_frame_navigation_without_load_dispatch(
|
||||
|
||||
@@ -54,7 +54,7 @@ impl JsContextHost {
|
||||
self.lightweight_popup_id_for_document_handle(owner_document)
|
||||
}
|
||||
|
||||
fn collect_child_browsing_context_handles_in_document_order_from_document(
|
||||
pub(in crate::native_bridge::context_host) fn collect_child_browsing_context_handles_in_document_order_from_document(
|
||||
&self,
|
||||
document: DomHandle,
|
||||
out: &mut Vec<DomHandle>,
|
||||
|
||||
Reference in New Issue
Block a user