fix(parser): respect script nesting for stylesheet waits

This commit is contained in:
ldm0
2026-09-23 00:14:09 +08:00
parent 91368fe52f
commit 51455017c5
7 changed files with 300 additions and 3 deletions
@@ -7586,6 +7586,7 @@ html/semantics/scripting-1/the-script-element/json-module/parse-error.html
html/semantics/scripting-1/the-script-element/json-module/script-element-json-src.html
html/semantics/scripting-1/the-script-element/json-module/valid-content-type.html
html/semantics/scripting-1/the-script-element/load-error-events-1.html
html/semantics/scripting-1/the-script-element/load-error-events-3.html
html/semantics/scripting-1/the-script-element/microtasks/checkpoint-after-window-onerror-module.html
html/semantics/scripting-1/the-script-element/microtasks/checkpoint-after-window-onerror.html
html/semantics/scripting-1/the-script-element/microtasks/checkpoint-after-workerglobalscope-onerror.html
@@ -0,0 +1,5 @@
styleNestingEvents.push('script');
window.styleNestingRan = true;
window.styleNestingSheetAtRun = document.getElementById('gate').sheet !== null;
window.styleNestingCurrentScript = document.currentScript.id;
document.write('<span id="written">ok</span>');
+3
View File
@@ -40,6 +40,9 @@ mod document_write_insertion;
#[path = "scripts/document_write_errors.rs"]
mod document_write_errors;
#[path = "scripts/document_write_stylesheets.rs"]
mod document_write_stylesheets;
#[path = "scripts/parser_cleanup.rs"]
mod parser_cleanup;
@@ -0,0 +1,263 @@
use super::*;
const SCRIPT: &str = include_str!("../fixtures/runtime/document_write_stylesheet_probe.js");
const INITIALIZE: &str = "window.styleNestingEvents = []; window.styleNestingRan = false;";
const OBSERVE_AND_RELEASE: &str = r#"
window.styleNestingRanAtReturn = styleNestingRan;
window.styleNestingRestoredScript = document.currentScript?.id ?? null;
styleNestingEvents.push('after-write');
fetch('/assets/dynamic_blocking_stylesheet_script_executed');
"#;
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()
}
fn script_markup(id: &str, source: &str, external: bool) -> String {
if external {
let source = url::form_urlencoded::byte_serialize(source.as_bytes())
.collect::<String>()
.replace('+', "%20");
format!("<script id={id} src=\"data:text/javascript,{source}\"></script>")
} else {
format!("<script id={id}>{source}</script>")
}
}
async fn stylesheet_wait_respects_parser_script_nesting(
child: bool,
script_created: bool,
nested: bool,
external: bool,
outer_external: bool,
) -> Result<()> {
let server = FixtureServer::spawn().await?;
let browser = Browser::new(AppConfig::default())?;
let script = script_markup("inner", SCRIPT, external);
let written = format!(
"<link id=gate rel=stylesheet href=/assets/dynamic_blocking_stylesheet_gated.css>{script}"
);
let fixture = if nested {
let written = serde_json::to_string(&written)?.replace('<', "\\u003c");
let outer = script_markup(
"outer",
&format!("{INITIALIZE}document.write({written});{OBSERVE_AND_RELEASE}"),
outer_external,
);
format!("<!doctype html><head>{outer}</head><body><main id=tail>tail</main>")
} else {
format!("<!doctype html><head>{written}</head><body><main id=tail>tail</main>")
};
let initial = if script_created {
"<!doctype html><body>"
} else {
&fixture
};
let initial = if child {
format!(
"<!doctype html><body><iframe id=target src=\"{}\"></iframe>",
markup_url(&server, initial)
)
} else {
initial.to_owned()
};
let mut page = tokio::time::timeout(
Duration::from_secs(10),
browser.fetch(&markup_url(&server, &initial)),
)
.await??;
let receiver = if child {
"document.getElementById('target').contentWindow"
} else {
"window"
};
if script_created {
let initialize = if nested { "" } else { INITIALIZE };
let observe = if nested { "" } else { OBSERVE_AND_RELEASE };
let expression = format!(
r#"new Promise(resolve => {{
const win = {receiver};
const doc = win.document;
doc.open();
win.eval({});
doc.addEventListener('DOMContentLoaded', () => resolve(true), {{once: true}});
doc.write({});
win.eval({});
doc.close();
}})"#,
serde_json::to_string(initialize)?,
serde_json::to_string(&fixture)?,
serde_json::to_string(observe)?,
);
tokio::time::timeout(
Duration::from_secs(10),
page.evaluate_runtime_expression_with_await_async(&expression, true),
)
.await??;
}
let observed = page
.evaluate_runtime_expression_with_await_async(
&format!(
r#"(() => {{
const win = {receiver};
return JSON.stringify({{
events: win.styleNestingEvents,
ranAtReturn: win.styleNestingRanAtReturn,
sheetAtRun: win.styleNestingSheetAtRun,
currentScript: win.styleNestingCurrentScript,
restoredScript: win.styleNestingRestoredScript,
written: win.document.getElementById('written')?.textContent,
tail: win.document.getElementById('tail')?.textContent
}});
}})()"#
),
true,
)
.await?;
let observed: serde_json::Value = serde_json::from_str(
observed["value"]
.as_str()
.expect("stylesheet nesting observation"),
)?;
let immediate = nested && !external;
assert_eq!(
observed,
serde_json::json!({
"events": if immediate { ["script", "after-write"] } else { ["after-write", "script"] },
"ranAtReturn": immediate,
"sheetAtRun": !immediate,
"currentScript": "inner",
"restoredScript": if nested { Some("outer") } else { None },
"written": "ok",
"tail": "tail"
}),
"child={child}, script_created={script_created}, nested={nested}, external={external}, outer_external={outer_external}"
);
server.shutdown().await;
Ok(())
}
macro_rules! stylesheet_nesting_test {
($name:ident, $child:expr, $created:expr, $nested:expr, $external:expr) => {
stylesheet_nesting_test!($name, $child, $created, $nested, $external, false);
};
($name:ident, $child:expr, $created:expr, $nested:expr, $external:expr, $outer_external:expr) => {
#[tokio::test(flavor = "multi_thread")]
async fn $name() -> Result<()> {
stylesheet_wait_respects_parser_script_nesting(
$child,
$created,
$nested,
$external,
$outer_external,
)
.await
}
};
}
stylesheet_nesting_test!(
main_nested_inline_ignores_stylesheet_wait,
false,
false,
true,
false
);
stylesheet_nesting_test!(
child_nested_inline_ignores_stylesheet_wait,
true,
false,
true,
false
);
stylesheet_nesting_test!(
main_open_nested_inline_ignores_stylesheet_wait,
false,
true,
true,
false
);
stylesheet_nesting_test!(
child_open_nested_inline_ignores_stylesheet_wait,
true,
true,
true,
false
);
stylesheet_nesting_test!(
main_nested_external_keeps_stylesheet_wait,
false,
false,
true,
true
);
stylesheet_nesting_test!(
child_nested_external_keeps_stylesheet_wait,
true,
false,
true,
true
);
stylesheet_nesting_test!(
main_open_nested_external_keeps_stylesheet_wait,
false,
true,
true,
true
);
stylesheet_nesting_test!(
child_open_nested_external_keeps_stylesheet_wait,
true,
true,
true,
true
);
stylesheet_nesting_test!(
main_open_direct_inline_keeps_stylesheet_wait,
false,
true,
false,
false
);
stylesheet_nesting_test!(
child_open_direct_inline_keeps_stylesheet_wait,
true,
true,
false,
false
);
stylesheet_nesting_test!(
main_external_outer_inline_ignores_stylesheet_wait,
false,
false,
true,
false,
true
);
stylesheet_nesting_test!(
child_external_outer_inline_ignores_stylesheet_wait,
true,
false,
true,
false,
true
);
stylesheet_nesting_test!(
main_open_external_outer_inline_ignores_stylesheet_wait,
false,
true,
true,
false,
true
);
stylesheet_nesting_test!(
child_open_external_outer_inline_ignores_stylesheet_wait,
true,
true,
true,
false,
true
);
+4
View File
@@ -291,6 +291,10 @@ impl ParserConnectedScriptBridge {
self.parser_control.enter_parser_script_nesting()
}
pub(crate) fn parser_script_nesting_level(&self) -> usize {
self.parser_control.parser_script_nesting_level()
}
pub(crate) fn run_state(&self) -> DocumentParserRunState {
self.parser_control.run_state()
}
@@ -2268,9 +2268,16 @@ impl DocumentRuntime {
blocking_signatures_before,
script,
} => {
if self.has_pending_parser_script_blocking_stylesheet_signatures(
blocking_signatures_before.iter(),
) {
// Preparing an inline script while another parser script runs raises
// HTML's script nesting level above one, so it executes immediately.
let nested_inline =
matches!(script.source, crate::planning::ScriptSource::Inline(_))
&& parser_bridge.parser_script_nesting_level() > 0;
if !nested_inline
&& self.has_pending_parser_script_blocking_stylesheet_signatures(
blocking_signatures_before.iter(),
)
{
if matches!(script.source, crate::planning::ScriptSource::External) {
self.start_document_write_stylesheet_blocked_external_script(
scope,
@@ -1697,6 +1697,7 @@ impl JsContextHost {
let ParserScriptHandoff::BlockingClassic {
node_id,
start_line,
blocking_signatures_before,
script,
..
} = handoff
@@ -1707,6 +1708,19 @@ impl JsContextHost {
(ScriptKind::Classic, ScriptSource::Inline(source)) => source.clone(),
_ => return false,
};
// Only nested inline parser scripts bypass blocking stylesheets. A direct
// document.write() into a script-created parser must wait for them.
if !self.child_document_is_executing_parser_script(document_handle)
&& self
.frame_owner_store
.current_child_document_owner(child_handle)
.is_some_and(|owner| {
self.frame_document_blocking_stylesheets
.blocks_signatures(owner, blocking_signatures_before.iter())
})
{
return false;
}
let Some(mut job) = self.frame_owner_child_parser_classic_script_job(
child_handle,
Some(*node_id),