Files
moli/moli-core/tests/document_open_unload.rs
T

182 lines
7.6 KiB
Rust

use anyhow::Result;
use moli_core::runtime::{Browser, BrowserConfig};
use moli_test_support::FixtureServer;
use serde_json::Value;
use tokio::time::Duration;
use url::Url;
async fn stream_operation_during_unload(
event: &str,
operation: &str,
target: &str,
) -> Result<Value> {
let server = FixtureServer::spawn().await?;
let browser = Browser::new(BrowserConfig::default())?;
let markup = format!(
r#"<!doctype html><body><script>
window.finished = (async () => {{
let nextFrameId = 0;
async function frameIn(owner) {{
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><p id="retained">retained') +
'&frame=' + nextFrameId++;
owner.document.body.append(frame);
await loaded;
return frame;
}}
const frame = await frameIn(window);
const target = {target:?};
let listenerFrame = frame;
let doc = frame.contentDocument;
if (target === 'other') doc = (await frameIn(window)).contentDocument;
if (target.endsWith('ancestor')) {{
const middle = await frameIn(frame.contentWindow);
doc = middle.contentDocument;
listenerFrame = await frameIn(middle.contentWindow);
}}
const root = doc.documentElement;
const url = doc.URL;
const originalLength = doc.childNodes.length;
let listenerCount = 0;
doc.addEventListener('retained-listener', () => listenerCount++);
let finish;
const result = new Promise(resolve => finish = resolve);
const listener = () => {{
let converted = 0;
const input = {{toString() {{ converted++; return '<body>overwritten'; }}}};
const operation = {operation:?};
let returnedDocument = false;
let error = null;
try {{
if (operation === 'open') returnedDocument = doc.open() === doc;
else if (operation === 'prototype-open')
returnedDocument = Document.prototype.open.call(doc) === doc;
else if (operation === 'prototype-write')
Document.prototype.write.call(doc, input);
else doc[operation](input);
}} catch (exception) {{ error = exception.name; }}
doc.dispatchEvent(new Event('retained-listener'));
finish({{sameRoot: doc.documentElement === root,
sameLength: doc.childNodes.length === originalLength,
sameURL: doc.URL === url, listenerCount,
converted, returnedDocument, error}});
if (target === 'other' || target === 'synthetic') {{
doc.write('<body>finished');
doc.close();
}}
}};
const event = {event:?};
const listenerTarget = event === 'visibilitychange' ?
listenerFrame.contentDocument : listenerFrame.contentWindow;
listenerTarget.addEventListener(event, listener, {{once: true}});
if (target === 'synthetic') listenerTarget.dispatchEvent(new Event(event));
else frame.src = target === 'ancestor' ? 'javascript:"<body>destination"' :
'/compat/child-dynamic-markup-document?markup=' +
encodeURIComponent('<!doctype html><body>destination');
return result;
}})();
</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!(
result["error"].is_null(),
"{event}/{operation}/{target}: {result}"
);
Ok(result)
}
#[tokio::test(flavor = "multi_thread")]
async fn document_stream_operations_have_no_side_effects_during_unload() -> Result<()> {
for event in ["beforeunload", "pagehide", "visibilitychange", "unload"] {
for operation in [
"open",
"prototype-open",
"write",
"writeln",
"prototype-write",
] {
let result = stream_operation_during_unload(event, operation, "self").await?;
assert_eq!(result["sameRoot"], true, "{event}/{operation}: {result}");
assert_eq!(result["sameLength"], true);
assert_eq!(result["sameURL"], true);
assert_eq!(result["listenerCount"], 1);
let is_open = operation.ends_with("open");
assert_eq!(result["returnedDocument"], is_open);
assert_eq!(result["converted"], usize::from(!is_open));
}
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn ancestor_unload_counter_covers_descendant_callbacks() -> Result<()> {
for event in ["pagehide", "visibilitychange", "unload"] {
let result = stream_operation_during_unload(event, "open", "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 ordinary_ancestor_unload_counter_covers_descendant_callbacks() -> Result<()> {
for event in ["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 beforeunload_counter_does_not_suppress_writes_to_an_ancestor() -> Result<()> {
let result =
stream_operation_during_unload("beforeunload", "open", "ordinary-ancestor").await?;
assert_eq!(result["sameRoot"], false, "{result}");
assert_eq!(result["sameLength"], false);
assert_eq!(result["sameURL"], false);
assert_eq!(result["listenerCount"], 0);
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", "visibilitychange", "unload"] {
let result = stream_operation_during_unload(event, "open", "other").await?;
assert_eq!(result["sameRoot"], false, "{event}: {result}");
assert_eq!(result["listenerCount"], 0);
assert_eq!(result["returnedDocument"], true);
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn synthetic_unload_events_do_not_block_document_open() -> Result<()> {
for event in ["beforeunload", "pagehide", "visibilitychange", "unload"] {
let result = stream_operation_during_unload(event, "open", "synthetic").await?;
assert_eq!(result["sameRoot"], false, "{event}: {result}");
assert_eq!(result["listenerCount"], 0);
assert_eq!(result["returnedDocument"], true);
}
Ok(())
}