mirror of
https://github.com/lexmount/moli.git
synced 2026-10-07 00:01:00 +00:00
fix(parser): respect script nesting for stylesheet waits
This commit is contained in:
@@ -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>');
|
||||
@@ -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
|
||||
);
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user