diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt
index cd82e2ac6b..7a3ceb06a6 100644
--- a/moli-benchmark/wpt-cross-current/passed-cases.txt
+++ b/moli-benchmark/wpt-cross-current/passed-cases.txt
@@ -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
diff --git a/moli-core/tests/fixtures/runtime/document_write_stylesheet_probe.js b/moli-core/tests/fixtures/runtime/document_write_stylesheet_probe.js
new file mode 100644
index 0000000000..72546e4c9a
--- /dev/null
+++ b/moli-core/tests/fixtures/runtime/document_write_stylesheet_probe.js
@@ -0,0 +1,5 @@
+styleNestingEvents.push('script');
+window.styleNestingRan = true;
+window.styleNestingSheetAtRun = document.getElementById('gate').sheet !== null;
+window.styleNestingCurrentScript = document.currentScript.id;
+document.write('ok');
diff --git a/moli-core/tests/scripts.rs b/moli-core/tests/scripts.rs
index 469e0cbf67..82c5b0a764 100644
--- a/moli-core/tests/scripts.rs
+++ b/moli-core/tests/scripts.rs
@@ -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;
diff --git a/moli-core/tests/scripts/document_write_stylesheets.rs b/moli-core/tests/scripts/document_write_stylesheets.rs
new file mode 100644
index 0000000000..cd51e74278
--- /dev/null
+++ b/moli-core/tests/scripts/document_write_stylesheets.rs
@@ -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::()
+ .replace('+', "%20");
+ format!("")
+ } else {
+ format!("")
+ }
+}
+
+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!(
+ "{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!("{outer}tail")
+ } else {
+ format!("{written}tail")
+ };
+ let initial = if script_created {
+ ""
+ } else {
+ &fixture
+ };
+ let initial = if child {
+ format!(
+ "",
+ 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
+);
diff --git a/moli-renderer-v8/src/document_runtime.rs b/moli-renderer-v8/src/document_runtime.rs
index 023cb67924..f7b8ad895e 100644
--- a/moli-renderer-v8/src/document_runtime.rs
+++ b/moli-renderer-v8/src/document_runtime.rs
@@ -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()
}
diff --git a/moli-renderer-v8/src/document_runtime/document_write.rs b/moli-renderer-v8/src/document_runtime/document_write.rs
index 0bd58c3fa3..a3c4eb86a1 100644
--- a/moli-renderer-v8/src/document_runtime/document_write.rs
+++ b/moli-renderer-v8/src/document_runtime/document_write.rs
@@ -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,
diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents/live_parser.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents/live_parser.rs
index 5459b4d278..ba422d7900 100644
--- a/moli-renderer-v8/src/native_bridge/context_host/child_documents/live_parser.rs
+++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents/live_parser.rs
@@ -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),