fix(style): process parser styles after their children finish

This commit is contained in:
ldm0
2026-09-27 06:43:24 +08:00
parent 69e2dc0c31
commit c06e7fdfe5
3 changed files with 112 additions and 0 deletions
+41
View File
@@ -2566,6 +2566,47 @@ mod tests {
);
}
#[test]
fn parser_style_children_finish_at_close_and_eof() {
for (opening, closing) in [
("<html><head><style id='sheet'>", "</style>"),
("<html><body><svg><style id='sheet'>", "</style></svg>"),
("<html><head><style id='sheet'>", ""),
] {
let mut stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
Url::parse("https://example.test/page").unwrap(),
);
stream.pump_parser_step(&format!("{opening}p {{ col"));
let document = stream.snapshot_parser_stream_document().into_document();
let style =
document.elements_by_tag_name(document.document_node_id(), "style", false)[0];
assert!(
document
.node(style)
.and_then(Node::as_element)
.unwrap()
.style_children_parsing()
);
stream.pump_parser_step(&format!("or: blue; }}{closing}"));
let document = if closing.is_empty() {
stream.finish()
} else {
stream.snapshot_parser_stream_document().into_document()
};
assert!(
!document
.node(style)
.and_then(Node::as_element)
.unwrap()
.style_children_parsing()
);
assert_eq!(
document.text_content(style).as_deref(),
Some("p { color: blue; }")
);
}
}
#[test]
fn parser_stream_reports_split_connected_meta_csp_once_and_ignores_template_contents() {
let stream = DocumentStream::new_scripting_enabled_parser_stream_for_testing(
+18
View File
@@ -784,6 +784,24 @@ mod tests {
use moli_dom::native::{DomHost, NativeDom, NativeNodeId, Node, NodeType};
use url::Url;
#[test]
fn xml_style_children_finish_at_close() {
for namespace in ["http://www.w3.org/1999/xhtml", "http://www.w3.org/2000/svg"] {
let document = XmlParser.parse(
Url::parse("https://example.test/style.xml").unwrap(),
format!("<style xmlns='{namespace}'>p {{ color: blue; }}</style>"),
);
let root = document.document_element_node_id().unwrap();
assert!(
!document
.node(root)
.and_then(Node::as_element)
.unwrap()
.style_children_parsing()
);
}
}
#[test]
fn xml_parser_records_unclosed_and_mismatched_elements() {
for (index, source) in [
@@ -6106,6 +6106,59 @@ globalThis.__outerContinued = true;
}));
}
#[test]
fn parser_style_csp_checks_complete_contents_once() {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime");
runtime.block_on(tokio::task::LocalSet::new().run_until(async {
for (markup, policy, report_only, expected_violations, expected_sheet) in [
("<style id='sheet'>p {color:blue;}</style>", "style-src 'none'", false, 1, false),
("<style id='sheet'>p {color:blue;}</style>", "style-src 'none'", true, 1, true),
("<style id='sheet'>p {color:blue;}</style>", "style-src 'sha256-rB6kiow2O3eFUeTNyyLeK3wV0+l7vNB90J1aqllKvjg='", false, 0, true),
("<style id='sheet'></style>", "style-src 'none'", false, 1, false),
("<style id='sheet'>p {color:blue;}", "style-src 'none'", false, 1, false),
("<script>document.write(\"<style id='sheet'>p {color:blue;}</style>\")</script>", "style-src 'none'", false, 1, false),
("<script>const s = document.createElement('style'); s.id = 'sheet'; document.head.append(s); s.textContent = 'p {color:blue;}';</script>", "style-src 'none'", false, 2, false),
] {
let env = default_test_page_vm_env_config_with(|env| {
let policies = if report_only {
&mut env.document_policy_container.response_content_security_report_only_policies
} else {
&mut env.document_policy_container.response_content_security_policies
};
*policies = vec![policy.to_owned()];
});
let html = Box::leak(format!("<!doctype html><html><head><script>globalThis.__parserStyleViolations = 0; document.addEventListener('securitypolicyviolation', () => __parserStyleViolations++);</script>{markup}").into_boxed_str());
let mut page_vm = parse_phase_one_html_into_page_vm_for_test_with_env_and_finish(html, env, true).await;
let result = page_vm.evaluate_expression("Boolean(document.getElementById('sheet').sheet)").expect("stylesheet probe");
assert_eq!(result.get("value").and_then(serde_json::Value::as_bool), Some(expected_sheet), "{markup}, {policy}, report-only={report_only}");
let local_executor = page_vm.local_executor.clone();
let page_vm_ptr: *mut PageVm = &mut page_vm;
super::access::run_named_owner_local_task(
local_executor,
"parser style CSP task channel closed",
async move {
let page_vm = unsafe { &mut *page_vm_ptr };
page_vm.page_task_queue.accept_ready_parse_time_wakes();
page_vm.vm_mut().drain_pre_domcontentloaded_content_security_policy_violation_tasks_for_test();
while let Some(task) = page_vm.page_task_queue.parse_time_pop_front() {
let work = PostParsePageOwnedWork::lifecycle_work(
crate::page_task_queue::PostParseLifecycleWork::from_parse_time_page_task(task),
);
execute_page_owned_work_turn_on_local_task(page_vm, work).await?;
}
Ok(())
},
).await.expect("parser style tasks");
let result = page_vm.evaluate_expression("__parserStyleViolations").expect("violation event count");
assert_eq!(result.get("value").and_then(serde_json::Value::as_u64), Some(expected_violations), "{markup}, {policy}, report-only={report_only}");
}
}));
}
#[test]
fn buffered_script_preload_cache_reuses_ready_late_parser_blocking_preload() {
let runtime = tokio::runtime::Builder::new_current_thread()