diff --git a/moli-renderer-v8/src/native_bridge/document.rs b/moli-renderer-v8/src/native_bridge/document.rs index b1957e898f..8c99d15be0 100644 --- a/moli-renderer-v8/src/native_bridge/document.rs +++ b/moli-renderer-v8/src/native_bridge/document.rs @@ -129,6 +129,8 @@ pub(super) use construction::{ }; pub(crate) use cookies::{document_cookie_for_receiver, set_document_cookie_for_receiver}; pub(crate) use css_state::install_adopted_style_sheets_array_primordials; +#[cfg(test)] +pub(crate) use css_state::take_owner_font_face_projection_count_for_test; pub(in crate::native_bridge) use css_state::{ AdoptedStyleSheetsArrayOwner, install_adopted_style_sheets_array_mutation_methods, normalize_adopted_style_sheets_assignment, diff --git a/moli-renderer-v8/src/native_bridge/document/css_state.rs b/moli-renderer-v8/src/native_bridge/document/css_state.rs index c33a41142c..f1e64eb287 100644 --- a/moli-renderer-v8/src/native_bridge/document/css_state.rs +++ b/moli-renderer-v8/src/native_bridge/document/css_state.rs @@ -3,6 +3,8 @@ mod font_faces; mod projection; mod shared; mod style_sheets; +#[cfg(test)] +pub(crate) use font_faces::take_owner_font_face_projection_count_for_test; pub(super) use accessors::detached_document_fonts_getter; pub(in crate::native_bridge) use accessors::{ diff --git a/moli-renderer-v8/src/native_bridge/document/css_state/font_faces.rs b/moli-renderer-v8/src/native_bridge/document/css_state/font_faces.rs index 29b9a77afe..44b97db6c0 100644 --- a/moli-renderer-v8/src/native_bridge/document/css_state/font_faces.rs +++ b/moli-renderer-v8/src/native_bridge/document/css_state/font_faces.rs @@ -23,6 +23,16 @@ const FONT_FACE_STYLESHEET_RULE_IDENTITY_SLOT: &str = "__moliFontFaceStylesheetR const FONT_FACE_STYLESHEET_RULE_FINGERPRINT_SLOT: &str = "__moliFontFaceStylesheetRuleFingerprint"; const FONT_FACE_STYLESHEET_ID_SLOT: &str = "__moliFontFaceStylesheetId"; +#[cfg(test)] +thread_local! { + static OWNER_FONT_FACE_PROJECTION_COUNT: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +#[cfg(test)] +pub(crate) fn take_owner_font_face_projection_count_for_test() -> usize { + OWNER_FONT_FACE_PROJECTION_COUNT.with(|count| count.replace(0)) +} + #[derive(Clone, Debug)] pub(super) enum OwnerFontFaceProjection { Descriptors(std::sync::Arc<[StylesheetFontFaceDescriptor]>), @@ -36,6 +46,8 @@ pub(super) fn owner_font_face_projection( host: &JsContextHost, owner: DomHandle, ) -> Option { + #[cfg(test)] + OWNER_FONT_FACE_PROJECTION_COUNT.with(|count| count.set(count.get() + 1)); let dom_host = host.dom_host(); let element = dom_host.node(owner)?.as_element()?; if !dom_host.is_connected(owner) @@ -144,13 +156,19 @@ pub(super) fn sync_document_fonts<'s>( pub(super) fn apply_font_face_owner_projection<'s>( scope: &mut v8::PinScope<'s, '_>, holder: v8::Local<'s, v8::Object>, + host: &JsContextHost, owner: DomHandle, - projection: Option<&OwnerFontFaceProjection>, + refresh: bool, ) -> bool { let Some(fonts) = object_property_as_object(scope, holder, FONTS_SLOT) else { return false; }; - set_owner_font_face_contribution(scope, fonts, owner, projection); + // Native CSS preparation and resource discovery have already run. Only + // construct the JS collection's projection when that collection exists. + let projection = refresh + .then(|| owner_font_face_projection(host, owner)) + .flatten(); + set_owner_font_face_contribution(scope, fonts, owner, projection.as_ref()); true } diff --git a/moli-renderer-v8/src/native_bridge/document/css_state/projection.rs b/moli-renderer-v8/src/native_bridge/document/css_state/projection.rs index 7516d95759..1a27bb0ba1 100644 --- a/moli-renderer-v8/src/native_bridge/document/css_state/projection.rs +++ b/moli-renderer-v8/src/native_bridge/document/css_state/projection.rs @@ -1,8 +1,5 @@ use super::{ - font_faces::{ - OwnerFontFaceProjection, apply_font_face_owner_projection, - finish_font_face_owner_projections, owner_font_face_projection, - }, + font_faces::{apply_font_face_owner_projection, finish_font_face_owner_projections}, style_sheets::sync_document_style_sheets, }; use crate::{ @@ -21,7 +18,7 @@ enum DocumentCssProjection { FontFaceOwner { document: DomHandle, owner: DomHandle, - projection: Option, + refresh: bool, }, } @@ -41,18 +38,18 @@ impl DocumentCssProjections { let owner = change.owner(); let scopes = change.tree_scopes(); if let Some(old_scope) = scopes.old() { - projections.push_for_scope(host, old_scope, owner, None); + projections.push_for_scope(host, old_scope, owner, false); } - let current_projection = match change.kind() { + let refresh = !matches!( + change.kind(), DomStylesheetOwnerChangeKind::TreeConnectionChanged { connected: false } - | DomStylesheetOwnerChangeKind::Unregistered => None, - _ => owner_font_face_projection(host, owner), - }; + | DomStylesheetOwnerChangeKind::Unregistered + ); if let Some(current_scope) = scopes.current_scope() { - projections.push_for_scope(host, current_scope, owner, current_projection.clone()); + projections.push_for_scope(host, current_scope, owner, refresh); } if let Some(new_scope) = scopes.new_scope() { - projections.push_for_scope(host, new_scope, owner, current_projection.clone()); + projections.push_for_scope(host, new_scope, owner, refresh); } } projections @@ -63,8 +60,7 @@ impl DocumentCssProjections { let Some(tree_scope) = host.dom_host().root_node_handle(owner) else { return projections; }; - let projection = owner_font_face_projection(host, owner); - projections.push_for_scope(host, tree_scope, owner, projection); + projections.push_for_scope(host, tree_scope, owner, true); projections } @@ -73,7 +69,7 @@ impl DocumentCssProjections { host: &JsContextHost, scope: DomHandle, owner: DomHandle, - projection: Option, + refresh: bool, ) { if !host.dom_host().node(scope).is_some_and(Node::is_document) { return; @@ -94,20 +90,20 @@ impl DocumentCssProjections { { let existing = &mut self.projections[index]; let DocumentCssProjection::FontFaceOwner { - projection: existing_projection, + refresh: existing_refresh, .. } = existing else { unreachable!(); }; - *existing_projection = projection; + *existing_refresh = refresh; return; } let index = self.projections.len(); self.projections.push(DocumentCssProjection::FontFaceOwner { document: scope, owner, - projection, + refresh, }); self.font_face_projection_indices .insert((scope, owner), index); @@ -133,14 +129,10 @@ impl DocumentCssProjections { DocumentCssProjection::FontFaceOwner { document, owner, - projection, + refresh, } => { - if apply_font_face_owner_projection( - scope, - holder, - owner, - projection.as_ref(), - ) && !font_face_documents.contains(&document) + if apply_font_face_owner_projection(scope, holder, host, owner, refresh) + && !font_face_documents.contains(&document) { font_face_documents.push(document); } diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/fonts.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/fonts.rs index a13b7ed911..fc8965907d 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/fonts.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/fonts.rs @@ -1323,3 +1323,61 @@ fn inline_stylesheet_copy_on_write_preserves_unmodified_font_face_wrappers() { r#"{"initialCount":2,"insertPreservedFirst":true,"insertPreservedSecond":true,"descriptorReplacedFirst":true,"descriptorPreservedSecond":true}"# ); } + +#[test] +fn css_owner_changes_prepare_font_projections_only_for_exposed_collections() { + use crate::native_bridge::document::take_owner_font_face_projection_count_for_test; + let mut vm = new_storage_test_vm("https://lazy-font-projection.test/"); + take_owner_font_face_projection_count_for_test(); + vm.eval( + r#" + globalThis.lazyStyle = document.createElement('style'); + (document.head || document.documentElement || document).appendChild(lazyStyle); + globalThis.heldSheets = document.styleSheets; + for (let i = 0; i < 64; i++) { + lazyStyle.textContent = `@font-face { font-family: Face${i}; src: local(Face${i}); } + #styled { color: rgb(1, 2, 3); }`; + } + globalThis.styled = document.createElement('div'); styled.id = 'styled'; + (document.body || document.documentElement || document).appendChild(styled); + 'ready' + "#, + ) + .unwrap(); + assert_eq!( + take_owner_font_face_projection_count_for_test(), + 0, + "unobserved FontFaceSets must not prepare owner projections" + ); + assert_eq!( + vm.eval("[heldSheets.length, getComputedStyle(styled).color].join('|')") + .unwrap(), + "1|rgb(1, 2, 3)" + ); + assert_eq!( + take_owner_font_face_projection_count_for_test(), + 0, + "native styling and exposed StyleSheetLists do not consume JS font projections" + ); + assert_eq!(vm.eval("globalThis.heldFonts = document.fonts; Array.from(heldFonts, face => face.family).join(',')").unwrap(), "Face63"); + assert_eq!( + take_owner_font_face_projection_count_for_test(), + 1, + "first observation prepares the current native state once" + ); + assert_eq!( + vm.eval( + r#" + lazyStyle.textContent = '@font-face { font-family: UpdatedFace; src: local(UpdatedFace); }'; + Array.from(heldFonts, face => face.family).join(',') + "# + ) + .unwrap(), + "UpdatedFace" + ); + assert!( + take_owner_font_face_projection_count_for_test() > 0, + "already held FontFaceSets must update without rereading Document.fonts" + ); + assert_eq!(vm.eval("lazyStyle.remove(); [heldFonts.size, heldSheets.length, heldFonts === document.fonts].join('|')").unwrap(), "0|0|true"); +}