mirror of
https://github.com/lexmount/moli.git
synced 2026-10-08 08:01:01 +00:00
fix(html): retain child parser EOF during script execution
This commit is contained in:
@@ -31,6 +31,9 @@ mod child_markup_insertion;
|
||||
#[path = "scripts/module_document_write.rs"]
|
||||
mod module_document_write;
|
||||
|
||||
#[path = "scripts/document_close.rs"]
|
||||
mod document_close;
|
||||
|
||||
#[path = "scripts/parser_cleanup.rs"]
|
||||
mod parser_cleanup;
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
use super::*;
|
||||
|
||||
fn markup_url(server: &FixtureServer, markup: &str) -> String {
|
||||
let mut url = url::Url::parse(&server.url("/compat/child-dynamic-markup-document")).unwrap();
|
||||
url.query_pairs_mut().append_pair("markup", markup);
|
||||
url.into()
|
||||
}
|
||||
|
||||
async fn written_child_script_close_finishes_load(
|
||||
character_chunks: bool,
|
||||
plaintext: bool,
|
||||
external: bool,
|
||||
) -> Result<()> {
|
||||
let server = FixtureServer::spawn().await?;
|
||||
let browser = Browser::new(AppConfig::default())?;
|
||||
let inserted = if plaintext {
|
||||
"<table><plaintext>Filler "
|
||||
} else {
|
||||
"<p id=inserted>inserted</p>"
|
||||
};
|
||||
let source = format!(
|
||||
r#"
|
||||
document.addEventListener('DOMContentLoaded', () => parent.events.push('child-DCL'));
|
||||
addEventListener('load', () => parent.events.push('child-load:' + document.readyState));
|
||||
document.write({});
|
||||
document.close();
|
||||
parent.events.push('close-return:' + document.readyState);
|
||||
"#,
|
||||
serde_json::to_string(inserted)?
|
||||
);
|
||||
let mut written = if external {
|
||||
format!(
|
||||
"<script src=\"data:text/javascript,{}\"></script>",
|
||||
url::form_urlencoded::byte_serialize(source.as_bytes())
|
||||
.collect::<String>()
|
||||
.replace('+', "%20")
|
||||
)
|
||||
} else {
|
||||
format!("<script>{source}</script>")
|
||||
};
|
||||
if !character_chunks {
|
||||
written.push_str("<main id=tail>tail</main>");
|
||||
}
|
||||
let parent = format!(
|
||||
r#"<!doctype html><body><script>
|
||||
window.events = [];
|
||||
addEventListener('load', () => events.push('parent-load:' + document.readyState));
|
||||
const frame = document.createElement('iframe');
|
||||
frame.id = 'target';
|
||||
document.body.append(frame);
|
||||
const written = {};
|
||||
if ({character_chunks}) {{
|
||||
for (const character of written) frame.contentDocument.write(character);
|
||||
}} else {{
|
||||
frame.contentDocument.write(written);
|
||||
}}
|
||||
</script>"#,
|
||||
serde_json::to_string(&written)?.replace("</script>", "<\\/script>")
|
||||
);
|
||||
// The written script is the only caller of close(). Fetch must finish both
|
||||
// the child and its load-blocked parent without another close from outside.
|
||||
let mut page = tokio::time::timeout(
|
||||
Duration::from_secs(10),
|
||||
browser.fetch(&markup_url(&server, &parent)),
|
||||
)
|
||||
.await??;
|
||||
let result = page
|
||||
.evaluate_runtime_expression_with_await_async(
|
||||
r#"(() => {
|
||||
const child = document.getElementById('target').contentDocument;
|
||||
return JSON.stringify({events, readyState: child.readyState,
|
||||
inserted: child.getElementById('inserted')?.textContent ?? null,
|
||||
tail: child.getElementById('tail')?.textContent ?? null,
|
||||
children: Array.from(child.body.children, node => [node.localName, node.textContent])});
|
||||
})()"#,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
let result: serde_json::Value =
|
||||
serde_json::from_str(result["value"].as_str().expect("child close observation"))?;
|
||||
assert_eq!(result["readyState"], "complete");
|
||||
assert_eq!(
|
||||
result["events"],
|
||||
serde_json::json!([
|
||||
"close-return:loading",
|
||||
"child-DCL",
|
||||
"child-load:complete",
|
||||
"parent-load:complete"
|
||||
])
|
||||
);
|
||||
if plaintext {
|
||||
assert_eq!(
|
||||
result["children"],
|
||||
serde_json::json!([["plaintext", "Filler "], ["table", ""]])
|
||||
);
|
||||
} else {
|
||||
assert_eq!(result["inserted"], "inserted");
|
||||
}
|
||||
if !character_chunks {
|
||||
assert_eq!(result["tail"], "tail");
|
||||
}
|
||||
server.shutdown().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn child_document_close_in_character_chunked_script_finishes_load() -> Result<()> {
|
||||
written_child_script_close_finishes_load(true, false, false).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn child_document_close_in_plaintext_writer_finishes_load() -> Result<()> {
|
||||
written_child_script_close_finishes_load(true, true, false).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn child_document_close_in_written_script_drains_tail_and_finishes_load() -> Result<()> {
|
||||
written_child_script_close_finishes_load(false, false, false).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn child_document_close_in_external_writer_drains_tail_and_finishes_load() -> Result<()> {
|
||||
written_child_script_close_finishes_load(false, false, true).await
|
||||
}
|
||||
@@ -1319,7 +1319,7 @@ impl JsContextHost {
|
||||
return true;
|
||||
}
|
||||
let executing_parser_script =
|
||||
chunk.is_some() && self.child_document_is_executing_parser_script(document_handle);
|
||||
self.child_document_is_executing_parser_script(document_handle);
|
||||
let nested_insertion = matches!(
|
||||
insertion.run_state(),
|
||||
DocumentParserRunState::Pumping { .. }
|
||||
@@ -1330,7 +1330,9 @@ impl JsContextHost {
|
||||
|| executing_parser_script
|
||||
|| nested_insertion);
|
||||
let ready = if close_requested {
|
||||
// Record EOF during parser-script execution and let its continuation finish.
|
||||
insertion.request_close() == DocumentParserCloseDisposition::DrainNow
|
||||
&& !executing_parser_script
|
||||
} else {
|
||||
matches!(
|
||||
insertion.run_state(),
|
||||
|
||||
@@ -448,6 +448,15 @@ fn child_document_close_callback<'s>(
|
||||
rv.set_undefined();
|
||||
return;
|
||||
}
|
||||
// Only script-created streams accept EOF from document.close().
|
||||
if !host
|
||||
.frame_owner_store
|
||||
.current_child_document_owner(handle)
|
||||
.is_some_and(|owner| host.child_document_parsers.has_open_stream(owner))
|
||||
{
|
||||
rv.set_undefined();
|
||||
return;
|
||||
}
|
||||
let script_context = match unsafe { &mut *host_ptr }
|
||||
.ensure_prebootstrapped_child_default_context(scope, handle)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user