mirror of
https://github.com/lexmount/moli.git
synced 2026-10-07 16:01:01 +00:00
fix(cssom): retain link sheets until replacement loads
Keep the installed owner source and JavaScript sheet while a valid link attribute change starts a successor load. Preserve existing CSSOM edits and prevent URL-cache hydration from replacing the current sheet prematurely. Replacement responses detach the old sheet at installation; integrity rejection retains it. Cover successful and failed loads, source bindings, and removal before a queued successor response settles.
This commit is contained in:
@@ -2825,7 +2825,6 @@ html/interaction/focus/the-autofocus-attribute/update-the-rendering.html
|
||||
html/semantics/document-metadata/the-link-element/link-error-fired-before-scripting-unblocked.html
|
||||
html/semantics/document-metadata/the-link-element/link-load-error-events.html
|
||||
html/semantics/document-metadata/the-link-element/link-load-fired-before-scripting-unblocked.html
|
||||
html/semantics/document-metadata/the-link-element/stylesheet-not-removed-until-next-stylesheet-loads.html
|
||||
html/semantics/document-metadata/the-style-element/tentative/style-element-basic-import.html
|
||||
html/semantics/document-metadata/the-style-element/tentative/style-element-csp-allowed.html
|
||||
html/semantics/document-metadata/the-style-element/tentative/style-element-csp-nonce-allowed.html
|
||||
|
||||
@@ -6231,6 +6231,7 @@ html/semantics/document-metadata/the-link-element/link-type-attribute-crash.html
|
||||
html/semantics/document-metadata/the-link-element/stylesheet-bad-mime-type.html
|
||||
html/semantics/document-metadata/the-link-element/stylesheet-media-change-no-fetch.html
|
||||
html/semantics/document-metadata/the-link-element/stylesheet-non-OK-status.html
|
||||
html/semantics/document-metadata/the-link-element/stylesheet-not-removed-until-next-stylesheet-loads.html
|
||||
html/semantics/document-metadata/the-meta-element/color-scheme/meta-color-scheme-attribute-changes.html
|
||||
html/semantics/document-metadata/the-meta-element/color-scheme/meta-color-scheme-empty-content-value.html
|
||||
html/semantics/document-metadata/the-meta-element/color-scheme/meta-color-scheme-first-valid-applies.html
|
||||
|
||||
@@ -1202,6 +1202,11 @@ impl JsContextHost {
|
||||
owner: DomHandle,
|
||||
request_url: &url::Url,
|
||||
) -> bool {
|
||||
// A loaded owner keeps its current sheet until the successor fetch
|
||||
// settles. Parsed URL cache hydration must not replace it in advance.
|
||||
if self.linked_stylesheet_source_for_owner(owner).is_some() {
|
||||
return false;
|
||||
}
|
||||
// A URL-only parsed source has no proof for this link's integrity.
|
||||
// Admission through the stylesheet fetch cache can reuse only a terminal
|
||||
// validated with the same request options, including integrity metadata.
|
||||
|
||||
@@ -237,6 +237,17 @@ fn owner_change_detaches_cached_link_sheet(
|
||||
element.attribute("title"),
|
||||
);
|
||||
}
|
||||
if matches!(
|
||||
change.kind(),
|
||||
DomStylesheetOwnerChangeKind::Attribute { .. }
|
||||
) {
|
||||
// The source lifecycle retains an installed sheet while its successor
|
||||
// loads. Preserve the corresponding JS object until an actual install
|
||||
// changes its identity, or the owner stops being a stylesheet.
|
||||
return host
|
||||
.linked_stylesheet_source_for_owner(change.owner())
|
||||
.is_none();
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
@@ -621,6 +621,194 @@ document.head.append(late);
|
||||
.expect("shared cache-hit stylesheet import test should run");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn stylesheet_reload_retains_cssom_until_the_successor_installs() {
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
||||
|
||||
run_page_vm_async_test(async move {
|
||||
let wrong = format!(
|
||||
"sha384-{}",
|
||||
STANDARD.encode(moli_crypto::DigestAlgorithm::Sha384.digest_bytes(b"wrong"))
|
||||
);
|
||||
for (status, integrity, expected) in [
|
||||
(
|
||||
"HTTP/1.1 200 OK",
|
||||
None,
|
||||
"false|false|false|1|1|2|rgb(4, 5, 6)|load",
|
||||
),
|
||||
(
|
||||
"HTTP/1.1 404 Not Found",
|
||||
None,
|
||||
"false|false|false|1|0|2|rgb(0, 0, 0)|error",
|
||||
),
|
||||
(
|
||||
"HTTP/1.1 200 OK",
|
||||
Some(wrong.as_str()),
|
||||
"true|true|true|1|2|2|rgb(7, 8, 9)|error",
|
||||
),
|
||||
] {
|
||||
let (base_url, server) = spawn_path_response_http_server(vec![
|
||||
(
|
||||
"/old.css",
|
||||
"HTTP/1.1 200 OK",
|
||||
"#reload-target { color: rgb(1, 2, 3); }".to_owned(),
|
||||
Duration::ZERO,
|
||||
),
|
||||
(
|
||||
"/next.css",
|
||||
status,
|
||||
"#reload-target { color: rgb(4, 5, 6); }".to_owned(),
|
||||
Duration::ZERO,
|
||||
),
|
||||
])
|
||||
.await;
|
||||
let loader =
|
||||
crate::network::ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
|
||||
let (mut page_vm, _resource_source, mut wake_rx) =
|
||||
page_vm_with_bound_task_sources_and_owner_wake(
|
||||
&loader,
|
||||
Url::parse(&format!("{base_url}/page.html"))?,
|
||||
);
|
||||
let old_href = serde_json::to_string(&format!("{base_url}/old.css"))?;
|
||||
let next_href = serde_json::to_string(&format!("{base_url}/next.css"))?;
|
||||
page_vm.vm_mut().eval(&format!(
|
||||
r#"
|
||||
globalThis.__reloadEvents = [];
|
||||
const target = document.body.appendChild(document.createElement("div"));
|
||||
target.id = "reload-target";
|
||||
const link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = {old_href};
|
||||
link.onload = () => __reloadEvents.push("load");
|
||||
link.onerror = () => __reloadEvents.push("error");
|
||||
document.head.append(link);
|
||||
"ready"
|
||||
"#,
|
||||
))?;
|
||||
|
||||
for stage in 0..2 {
|
||||
page_vm
|
||||
.vm_mut()
|
||||
.prime_document_lifecycle_processing_and_record_stylesheet_network_results();
|
||||
wait_for_stylesheet_source(&mut wake_rx, RendererOwnerWakeSource::NetworkingTask)
|
||||
.await;
|
||||
assert!(
|
||||
page_vm
|
||||
.run_exact_selected_page_task_for_test(
|
||||
PageSelectedTaskTestSelector::StylesheetCompletion,
|
||||
&loader,
|
||||
)
|
||||
.await?
|
||||
);
|
||||
let event = take_next_link_element_event_task_for_test(&mut page_vm)
|
||||
.expect("the selected stylesheet response must publish its result");
|
||||
page_vm
|
||||
.run_claimed_dom_manipulation_task_through_selected_dispatcher_for_test(
|
||||
crate::page_task_queue::RendererPageDomManipulationTask::ConnectedStyleEvent(
|
||||
event,
|
||||
),
|
||||
&loader,
|
||||
)
|
||||
.await?;
|
||||
if stage == 0 {
|
||||
assert_eq!(page_vm.vm_mut().eval("__reloadEvents.join('|')")?, "load");
|
||||
let integrity = serde_json::to_string(integrity.unwrap_or_default())?;
|
||||
let pending = page_vm.vm_mut().eval(&format!(
|
||||
r##"
|
||||
const old = link.sheet;
|
||||
old.insertRule("#reload-target {{ color: rgb(7, 8, 9); }}", old.cssRules.length);
|
||||
globalThis.__reloadSnapshot = () => [
|
||||
link.sheet === old, old.ownerNode === link, document.styleSheets[0] === old,
|
||||
document.styleSheets.length, link.sheet.cssRules.length, old.cssRules.length,
|
||||
getComputedStyle(target).color, __reloadEvents.join(",")
|
||||
].join("|");
|
||||
__reloadEvents.length = 0;
|
||||
link.integrity = {integrity};
|
||||
link.href = {next_href};
|
||||
__reloadSnapshot()
|
||||
"##,
|
||||
))?;
|
||||
assert_eq!(
|
||||
pending,
|
||||
"true|true|true|1|2|2|rgb(7, 8, 9)|",
|
||||
"changing href must retain the old JS sheet and its CSSOM edits until replacement"
|
||||
);
|
||||
} else {
|
||||
assert_eq!(page_vm.vm_mut().eval("__reloadSnapshot()")?, expected);
|
||||
assert_eq!(
|
||||
page_vm.vm_mut().eval(&format!("old.href === {old_href}"))?,
|
||||
"true",
|
||||
"the old sheet's URL must not follow the owner's new href"
|
||||
);
|
||||
assert!(!page_vm.vm().document_runtime.has_pending_style_loads());
|
||||
}
|
||||
}
|
||||
server.await.expect("both stylesheet responses should be consumed");
|
||||
}
|
||||
Ok::<_, anyhow::Error>(())
|
||||
})
|
||||
.await
|
||||
.expect("stylesheet reload CSSOM lifetimes should run");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn stylesheet_reload_removal_retires_the_sheet_and_pending_successor() {
|
||||
run_page_vm_async_test(async move {
|
||||
let (base_url, server) = spawn_path_response_http_server(vec![
|
||||
("/old.css", "HTTP/1.1 200 OK", "body { color: red; }".to_owned(), Duration::ZERO),
|
||||
("/next.css", "HTTP/1.1 200 OK", "body { color: green; }".to_owned(), Duration::ZERO),
|
||||
])
|
||||
.await;
|
||||
let loader =
|
||||
crate::network::ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
|
||||
let (mut page_vm, _resource_source, mut wake_rx) =
|
||||
page_vm_with_bound_task_sources_and_owner_wake(
|
||||
&loader,
|
||||
Url::parse(&format!("{base_url}/page.html"))?,
|
||||
);
|
||||
let old_href = serde_json::to_string(&format!("{base_url}/old.css"))?;
|
||||
let next_href = serde_json::to_string(&format!("{base_url}/next.css"))?;
|
||||
page_vm.vm_mut().eval(&format!(
|
||||
r#"
|
||||
globalThis.__reloadEvents = [];
|
||||
const link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = {old_href};
|
||||
link.onload = () => __reloadEvents.push("load");
|
||||
link.onerror = () => __reloadEvents.push("error");
|
||||
document.head.append(link);
|
||||
"ready"
|
||||
"#,
|
||||
))?;
|
||||
page_vm.vm_mut().prime_document_lifecycle_processing_and_record_stylesheet_network_results();
|
||||
wait_for_stylesheet_source(&mut wake_rx, RendererOwnerWakeSource::NetworkingTask).await;
|
||||
assert!(page_vm.run_exact_selected_page_task_for_test(PageSelectedTaskTestSelector::StylesheetCompletion, &loader).await?);
|
||||
let event = take_next_link_element_event_task_for_test(&mut page_vm).expect("initial load event");
|
||||
page_vm.run_claimed_dom_manipulation_task_through_selected_dispatcher_for_test(
|
||||
crate::page_task_queue::RendererPageDomManipulationTask::ConnectedStyleEvent(event), &loader,
|
||||
).await?;
|
||||
assert_eq!(page_vm.vm_mut().eval(&format!(
|
||||
"const old = link.sheet; __reloadEvents.length = 0; link.href = {next_href}; link.sheet === old && old.ownerNode === link"
|
||||
))?, "true");
|
||||
|
||||
page_vm.vm_mut().prime_document_lifecycle_processing_and_record_stylesheet_network_results();
|
||||
wait_for_stylesheet_source(&mut wake_rx, RendererOwnerWakeSource::NetworkingTask).await;
|
||||
assert_eq!(page_vm.vm_mut().eval(
|
||||
"link.remove(); [link.sheet === null, old.ownerNode === null, document.styleSheets.length].join('|')"
|
||||
)?, "true|true|0");
|
||||
let _ = page_vm.run_exact_selected_page_task_for_test(PageSelectedTaskTestSelector::StylesheetCompletion, &loader).await?;
|
||||
assert!(take_next_link_element_event_task_for_test(&mut page_vm).is_none());
|
||||
assert_eq!(page_vm.vm_mut().eval(
|
||||
"[link.sheet === null, old.ownerNode === null, document.styleSheets.length, __reloadEvents.length].join('|')"
|
||||
)?, "true|true|0|0");
|
||||
assert!(!page_vm.vm().document_runtime.has_pending_style_loads());
|
||||
server.await.expect("both requests reached the fixture before removal");
|
||||
Ok::<_, anyhow::Error>(())
|
||||
})
|
||||
.await
|
||||
.expect("removal must invalidate a reloading stylesheet owner");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn stylesheet_integrity_does_not_reuse_an_unverified_css_source() {
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
||||
|
||||
@@ -580,6 +580,26 @@ impl MoliStyleEngine {
|
||||
continue;
|
||||
}
|
||||
let previous_documents = self.linked_stylesheet_owner_documents(owner);
|
||||
if matches!(
|
||||
change.kind(),
|
||||
DomStylesheetOwnerChangeKind::Attribute { .. }
|
||||
) && crate::stylesheet_blocking::stylesheet_link_disposition(
|
||||
host,
|
||||
moli_dom::NodeId::new(owner.index()),
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
// Reprocessing a valid link starts its successor load. Keep the
|
||||
// installed sheet, including CSSOM edits, until that response
|
||||
// installs a replacement; integrity rejection installs nothing.
|
||||
self.invalidate_linked_stylesheet_owner_lifecycle_change(
|
||||
host,
|
||||
owner,
|
||||
previous_documents,
|
||||
owner_document_for_source_owner(host, owner),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
self.remove_linked_stylesheet_owner_from_documents(host, owner, previous_documents);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2080,11 +2080,10 @@ fn linked_owner_binding_changes_only_on_explicit_install() {
|
||||
let media = crate::protocol_types::EmulatedMediaOverrides::default();
|
||||
engine.invalidate_for_mutations(&host, &style_effects, &media);
|
||||
|
||||
assert!(
|
||||
engine
|
||||
.retained_stylesheet_source_ids_for_document_for_test(&host, document)
|
||||
.is_empty(),
|
||||
"the old loaded stylesheet must not remain bound after href changes"
|
||||
assert_eq!(
|
||||
engine.retained_stylesheet_source_ids_for_document_for_test(&host, document),
|
||||
vec![linked_source_id.clone()],
|
||||
"the loaded stylesheet remains bound while the new href is pending"
|
||||
);
|
||||
|
||||
engine.record_stylesheet_source_for_url_for_document_for_test(
|
||||
@@ -2092,17 +2091,32 @@ fn linked_owner_binding_changes_only_on_explicit_install() {
|
||||
&new_url,
|
||||
StyloStylesheetSource::new(".new { color: blue; }".into(), new_url.clone()),
|
||||
);
|
||||
assert!(
|
||||
assert_eq!(
|
||||
engine.retained_stylesheet_source_ids_for_document_for_test(&host, document),
|
||||
vec![linked_source_id.clone()],
|
||||
"recording a resource by URL must not replace the pending owner's sheet"
|
||||
);
|
||||
assert_eq!(
|
||||
engine
|
||||
.retained_stylesheet_source_ids_for_document_for_test(&host, document)
|
||||
.is_empty(),
|
||||
"recording a resource by URL must not infer an owner binding from live href"
|
||||
.linked_stylesheet_source_for_owner_with_host(&host, link)
|
||||
.unwrap()
|
||||
.serialized_css_text()
|
||||
.as_ref(),
|
||||
".old { color: red; }"
|
||||
);
|
||||
assert!(engine.install_recorded_linked_stylesheet_source_for_test(&host, link, &new_url,));
|
||||
assert_eq!(
|
||||
engine.retained_stylesheet_source_ids_for_document_for_test(&host, document),
|
||||
vec![linked_source_id]
|
||||
);
|
||||
assert_eq!(
|
||||
engine
|
||||
.linked_stylesheet_source_for_owner_with_host(&host, link)
|
||||
.unwrap()
|
||||
.serialized_css_text()
|
||||
.as_ref(),
|
||||
".new { color: blue; }"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user