fix(fonts): load string sources on demand

This commit is contained in:
ldm0
2026-09-27 06:43:16 +08:00
parent 67c36cacdd
commit fc8499110c
19 changed files with 339 additions and 43 deletions
@@ -1577,6 +1577,7 @@ css/css-flexbox/webkit-box-vertical-writing-mode.html
css/css-flexbox/whitespace-in-flexitem-001.html
css/css-flexbox/zero-content-size-with-scrollbar-crash.html
css/css-font-loading/empty-family-load.html
css/css-font-loading/font-face-reject.html
css/css-font-loading/fontface-font-variation-settings-persisted-js-api.html
css/css-font-loading/fontface-fonts-loading.html
css/css-font-loading/fontface-invalid-arraybuffer.html
@@ -8,9 +8,10 @@
document.fonts.onloading = (event) => events.push(event.type);
document.fonts.onloadingdone = (event) => events.push(event.type);
document.fonts.onloadingerror = (event) => events.push(event.type);
document.fonts.add(new FontFace('FixtureFace', 'url(fixture.woff2)'));
document.fonts
.load('10px sans-serif')
.load('10px FixtureFace')
.then(() => document.fonts.ready)
.then(() => {
body.setAttribute('data-order', events.join(','));
@@ -22,6 +22,7 @@
document.fonts.addEventListener('loading', onLoadingB);
document.fonts.addEventListener('loadingdone', onLoadingDone);
document.fonts.removeEventListener('loading', onLoadingA);
document.fonts.add(new FontFace('Roboto', 'url(roboto.woff2)'));
document.fonts.load('italic bold 16px Roboto');
document.body.setAttribute('data-loading-a', String(loadingA));
@@ -27,6 +27,7 @@
throw new Error("font handler boom");
};
try {
document.fonts.add(new FontFace("Roboto", "url(roboto.woff2)"));
document.fonts.load("italic bold 16px Roboto");
body.setAttribute("data-font-continued", "true");
} catch (error) {
@@ -14,6 +14,7 @@
const ready1 = document.fonts.ready;
body.setAttribute('data-ready-stable', String(ready1 === document.fonts.ready));
document.fonts.add(new FontFace('FixtureFace', 'url(fixture.woff2)'));
const loadPromise = document.fonts.load('10px FixtureFace');
const ready2 = document.fonts.ready;
body.setAttribute('data-ready-replaced', String(ready1 !== ready2));
+10 -10
View File
@@ -2459,16 +2459,16 @@ fn stylo_list_marker_type(computed: &ComputedValues) -> LayoutListMarkerType {
_ => LayoutListMarkerType::Fallback,
},
CounterStyle::String(value) => LayoutListMarkerType::String(Arc::from(value.as_ref())),
CounterStyle::Symbols { symbols, .. } => LayoutListMarkerType::Symbols(
symbols
.0
.iter()
.map(|symbol| match symbol {
Symbol::String(value) => Arc::from(value.as_ref()),
Symbol::Ident(value) => Arc::from(value.0.as_ref()),
})
.collect(),
),
CounterStyle::Symbols { symbols, .. } => {
let mut text_symbols = Vec::with_capacity(symbols.0.len());
for symbol in symbols.0.iter() {
match symbol {
Symbol::String(value) => text_symbols.push(Arc::from(value.as_ref())),
Symbol::Ident(value) => text_symbols.push(Arc::from(value.0.as_ref())),
}
}
LayoutListMarkerType::Symbols(text_symbols)
}
}
}
+3 -1
View File
@@ -137,7 +137,9 @@ pub(crate) use crypto::{
CryptoKeyAlgorithmClonePayload, CryptoKeyClonePayload, WebCryptoRejection, WebCryptoTaskResult,
crypto_key_clone_payload_from_object, crypto_key_object_from_clone_payload,
};
pub(crate) use css_fontface_runtime::{new_font_face_set, rebuild_font_face_set_faces};
pub(crate) use css_fontface_runtime::{
load_font_faces_for_family, new_font_face_set, rebuild_font_face_set_faces,
};
pub(crate) use location_navigation::{
LocationNavigationKind, dispatch_top_level_form_navigation_event,
dispatch_top_level_navigation_event_with_source_element, meta_refresh_navigation_kind,
@@ -25,6 +25,7 @@ pub(super) const FONT_FACE_LINE_GAP_OVERRIDE_SLOT: &str = "__moliFontFaceLineGap
pub(super) const FONT_FACE_SIZE_ADJUST_SLOT: &str = "__moliFontFaceSizeAdjust";
pub(super) const FONT_FACE_STATUS_SLOT: &str = "__moliFontFaceStatus";
pub(super) const FONT_FACE_LOADED_SLOT: &str = "__moliFontFaceLoaded";
pub(super) const FONT_FACE_LOADED_RESOLVER_SLOT: &str = "__moliFontFaceLoadedResolver";
pub(super) const FONT_FACE_SET_OWNERS_SLOT: &str = "__moliFontFaceSetOwners";
pub(super) const FONT_FACE_LOAD_NOTIFICATION_SENT_SLOT: &str = "__moliFontFaceLoadNotificationSent";
pub(super) const FONT_FACE_SET_MANUAL_FACES_SLOT: &str = "__moliFontFaceSetManualFaces";
@@ -44,6 +45,7 @@ pub(in crate::context_bootstrap) use events::{
initialize_font_face_set_load_event, install_font_face_set_event_handler_accessors,
install_font_face_set_load_event_template_accessors,
};
pub(crate) use font_face::load_font_faces_for_family;
pub(super) use font_face::{
font_face_constructor_callback, font_face_load_callback, install_font_face_template_accessors,
};
@@ -304,7 +304,7 @@ pub(super) fn notify_font_face_set_owners_of_load<'s>(
}
let loaded_faces = v8::Array::new(scope, 1);
let _ = loaded_faces.set_index(scope, 0, face.into());
let completion_event_type = if font_face_load_failed(scope, face) {
let completion_event_type = if super::font_face::font_face_load_failed(scope, face) {
"loadingerror"
} else {
"loadingdone"
@@ -318,12 +318,3 @@ pub(super) fn notify_font_face_set_owners_of_load<'s>(
dispatch_font_face_set_event(scope, owner, completion_event_type, Some(loaded_faces));
}
}
fn font_face_load_failed<'s>(
scope: &mut v8::PinScope<'s, '_>,
face: v8::Local<'s, v8::Object>,
) -> bool {
get_private_value(scope, face, FONT_FACE_STATUS_SLOT)
.and_then(|value| v8::Local::<v8::String>::try_from(value).ok())
.is_some_and(|status| status.to_rust_string_lossy(scope) == "error")
}
@@ -44,6 +44,8 @@ struct FontFaceObjectDeclaration<'s> {
status: &'static str,
#[webapi(slot = FONT_FACE_LOADED_SLOT)]
loaded: Option<v8::Local<'s, v8::Promise>>,
#[webapi(slot = FONT_FACE_LOADED_RESOLVER_SLOT)]
loaded_resolver: Option<v8::Local<'s, v8::PromiseResolver>>,
#[webapi(slot = FONT_FACE_SET_OWNERS_SLOT, constructor_default = Vec::new())]
owner_sets: Vec<v8::Local<'s, v8::Value>>,
#[webapi(slot = FONT_FACE_LOAD_NOTIFICATION_SENT_SLOT, constructor_default = false)]
@@ -305,7 +307,7 @@ pub(in crate::context_bootstrap) fn font_face_constructor_callback<'s>(
size_adjust,
] = descriptors;
let this = args.this();
let (source, status, loaded) = if invalid_descriptors {
let (source, status, loaded, loaded_resolver) = if invalid_descriptors {
let source = match parsed.source {
FontFaceConstructorSource::Css(source) => source,
FontFaceConstructorSource::Binary(_) => String::new(),
@@ -315,18 +317,30 @@ pub(in crate::context_bootstrap) fn font_face_constructor_callback<'s>(
"SyntaxError",
"Invalid FontFace descriptor.",
);
(source, "error", Some(loaded))
(source, "error", Some(loaded), None)
} else {
match parsed.source {
FontFaceConstructorSource::Css(source)
if moli_css_parse::normalize_font_face_src(&source).is_some() =>
{
let resolver = v8::PromiseResolver::new(scope)
.expect("FontFace loaded promise resolver should allocate");
let loaded = resolver.get_promise(scope);
(source, "unloaded", Some(loaded), Some(resolver))
}
FontFaceConstructorSource::Css(source) => {
let loaded = resolved_promise(scope, this.into());
(source, "loaded", loaded)
let loaded = super::query::make_rejected_dom_exception_promise(
scope,
"SyntaxError",
"Invalid FontFace source descriptor.",
);
(source, "error", Some(loaded), None)
}
FontFaceConstructorSource::Binary(bytes)
if moli_web_mime::sniff_font_mime_type(&bytes).is_some() =>
{
let loaded = resolved_promise(scope, this.into());
(String::new(), "loaded", loaded)
(String::new(), "loaded", loaded, None)
}
FontFaceConstructorSource::Binary(_) => {
let loaded = super::query::make_rejected_dom_exception_promise(
@@ -334,7 +348,7 @@ pub(in crate::context_bootstrap) fn font_face_constructor_callback<'s>(
"SyntaxError",
"Invalid font data in ArrayBuffer.",
);
(String::new(), "error", Some(loaded))
(String::new(), "error", Some(loaded), None)
}
}
};
@@ -355,6 +369,7 @@ pub(in crate::context_bootstrap) fn font_face_constructor_callback<'s>(
size_adjust,
status,
loaded,
loaded_resolver,
)
.initialize(scope, this)
.expect("FontFace declaration should initialize object");
@@ -395,8 +410,7 @@ pub(in crate::context_bootstrap) fn font_face_load_callback<'s>(
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
let this = args.this();
super::events::notify_font_face_set_owners_of_load(scope, this);
if let Some(loaded) = font_face_slot_value(scope, this, FONT_FACE_LOADED_SLOT) {
if let Some(loaded) = load_font_face(scope, this) {
rv.set(loaded);
return;
}
@@ -406,6 +420,117 @@ pub(in crate::context_bootstrap) fn font_face_load_callback<'s>(
}
}
pub(super) fn load_font_face<'s>(
scope: &mut v8::PinScope<'s, '_>,
face: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Value>> {
let loaded = font_face_slot_value(scope, face, FONT_FACE_LOADED_SLOT);
if font_face_status(scope, face).as_deref() != Some("unloaded") {
super::events::notify_font_face_set_owners_of_load(scope, face);
return loaded;
}
set_font_face_status(scope, face, "loading");
let source = font_face_string_slot(scope, face, FONT_FACE_SOURCE_SLOT).unwrap_or_default();
let succeeds = font_face_css_source_has_url(&source);
if succeeds {
set_font_face_status(scope, face, "loaded");
} else {
set_font_face_status(scope, face, "error");
}
if let Some(resolver) = font_face_loaded_resolver(scope, face) {
if succeeds {
let _ = resolver.resolve(scope, face.into());
} else {
let exception = crate::context_bootstrap::new_dom_exception_value(
scope,
"No source in the FontFace src list could be loaded.",
"NetworkError",
);
let _ = resolver.reject(scope, exception);
}
}
super::events::notify_font_face_set_owners_of_load(scope, face);
loaded
}
pub(super) fn font_face_load_failed<'s>(
scope: &mut v8::PinScope<'s, '_>,
face: v8::Local<'s, v8::Object>,
) -> bool {
font_face_status(scope, face).as_deref() == Some("error")
}
pub(crate) fn load_font_faces_for_family<'s>(
scope: &mut v8::PinScope<'s, '_>,
font_set: v8::Local<'s, v8::Object>,
family: &str,
) {
let Some(faces) = super::storage::font_face_set_faces_array(scope, font_set) else {
return;
};
for index in 0..faces.length() {
let Some(face) = faces
.get_index(scope, index)
.and_then(|face| v8::Local::<v8::Object>::try_from(face).ok())
else {
continue;
};
if !font_face_string_slot(scope, face, FONT_FACE_FAMILY_SLOT)
.is_some_and(|candidate| candidate.eq_ignore_ascii_case(family))
{
continue;
}
let _ = load_font_face(scope, face);
}
}
fn font_face_css_source_has_url(source: &str) -> bool {
moli_css_parse::normalize_font_face_src(source)
.and_then(|source| crate::css_style::top_level_comma_separated_component_values(&source))
.is_some_and(|sources| {
sources
.iter()
.any(|source| source.trim_start().starts_with("url("))
})
}
fn font_face_status<'s>(
scope: &mut v8::PinScope<'s, '_>,
face: v8::Local<'s, v8::Object>,
) -> Option<String> {
font_face_string_slot(scope, face, FONT_FACE_STATUS_SLOT)
}
fn font_face_string_slot<'s>(
scope: &mut v8::PinScope<'s, '_>,
face: v8::Local<'s, v8::Object>,
slot: &str,
) -> Option<String> {
font_face_slot_value(scope, face, slot)
.and_then(|value| v8::Local::<v8::String>::try_from(value).ok())
.map(|value| value.to_rust_string_lossy(scope))
}
fn set_font_face_status<'s>(
scope: &mut v8::PinScope<'s, '_>,
face: v8::Local<'s, v8::Object>,
status: &'static str,
) {
let status = v8_string(scope, status).unwrap_or_else(|| v8::String::empty(scope));
set_font_face_slot_value(scope, face, FONT_FACE_STATUS_SLOT, status.into());
}
fn font_face_loaded_resolver<'s>(
scope: &mut v8::PinScope<'s, '_>,
face: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::PromiseResolver>> {
font_face_slot_value(scope, face, FONT_FACE_LOADED_RESOLVER_SLOT)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
.map(|object| unsafe { v8::Local::<v8::PromiseResolver>::cast_unchecked(object) })
}
fn font_face_slot_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
@@ -1,12 +1,11 @@
use super::events::dispatch_font_face_set_event;
use super::font_face::{font_face_load_failed, load_font_face};
use super::query::{
font_face_set_matching_faces_array, font_load_query_contains_css_wide_keyword,
make_rejected_dom_exception_promise,
};
use super::storage::{
array_contains_value, font_face_set_faces_array, font_face_set_manual_faces_array,
is_font_face_value, rebuild_font_face_set_faces, replace_font_face_set_ready_promise,
set_font_face_set_slot_value, set_font_face_set_status,
is_font_face_value, rebuild_font_face_set_faces, set_font_face_set_slot_value,
};
use super::*;
@@ -56,11 +56,28 @@ pub(in crate::context_bootstrap) fn font_face_set_load_callback<'s>(
}
let matching_faces = font_face_set_matching_faces_array(scope, this, &parsed.font)
.unwrap_or_else(|| v8::Array::new(scope, 0));
set_font_face_set_status(scope, this, "loading");
let _ = dispatch_font_face_set_event(scope, this, "loading", None);
replace_font_face_set_ready_promise(scope, this);
set_font_face_set_status(scope, this, "loaded");
let _ = dispatch_font_face_set_event(scope, this, "loadingdone", Some(matching_faces));
let mut failed = false;
for index in 0..matching_faces.length() {
let Some(face) = matching_faces
.get_index(scope, index)
.and_then(|face| v8::Local::<v8::Object>::try_from(face).ok())
else {
continue;
};
let _ = load_font_face(scope, face);
failed |= font_face_load_failed(scope, face);
}
if failed {
rv.set(
make_rejected_dom_exception_promise(
scope,
"NetworkError",
"One or more matching FontFace objects failed to load.",
)
.into(),
);
return;
}
let faces_value = matching_faces.into();
match resolved_promise(scope, faces_value) {
Some(promise) => rv.set(v8::Local::<v8::Value>::from(promise)),
@@ -1844,6 +1844,7 @@ pub(crate) struct RuntimeMutationApplyResult {
stylesheet_owner_changes: Vec<crate::dom::native::DomStylesheetOwnerChange>,
inline_style_attribute_csp_mutations: Vec<InlineStyleAttributeCspMutation>,
connected_style_csp_roots: Vec<DomHandle>,
font_face_use_roots: Vec<DomHandle>,
}
#[derive(Clone, Debug)]
@@ -1880,6 +1881,7 @@ pub(super) fn finish_runtime_mutation_effects(
stylesheet_owner_changes,
inline_style_attribute_csp_mutations,
connected_style_csp_roots,
font_face_use_roots,
} = result;
runtime.queue_devtools_dom_mutations(devtools_dom_mutations);
@@ -1976,6 +1978,11 @@ pub(super) fn finish_runtime_mutation_effects(
unsafe { &*host_ptr },
&stylesheet_owner_changes,
);
crate::native_bridge::document::load_font_faces_used_by_subtrees(
scope,
unsafe { &*host_ptr },
&font_face_use_roots,
);
let completed_clients = prime_result.take_completed_stylesheet_clients();
runtime.settle_stylesheet_link_clients_in_current_scope(scope, host_ptr, completed_clients);
runtime
@@ -2215,6 +2222,15 @@ pub(super) fn prepare_runtime_mutation_effects(
};
let devtools_dom_mutations =
super::devtools_mutations::capture_devtools_dom_mutation_facts(dom_host, effects);
let mut font_face_use_roots = effects.tree().connected_roots().to_vec();
for mutation in effects.style().attribute_mutations() {
if mutation.namespace().is_none()
&& mutation.local_name().eq_ignore_ascii_case("style")
&& !font_face_use_roots.contains(&mutation.target())
{
font_face_use_roots.push(mutation.target());
}
}
RuntimeMutationApplyResult {
changed: effects.did_change(),
meta_refresh_candidates,
@@ -2225,6 +2241,7 @@ pub(super) fn prepare_runtime_mutation_effects(
stylesheet_owner_changes,
inline_style_attribute_csp_mutations,
connected_style_csp_roots,
font_face_use_roots,
}
}
@@ -137,7 +137,8 @@ pub(in crate::native_bridge) use css_state::{
};
pub(crate) use css_state::{
apply_stylesheet_owner_css_projections, apply_stylesheet_source_css_projection,
clear_adopted_stylesheet_font_face_wrappers, sync_document_fonts_for_handle,
clear_adopted_stylesheet_font_face_wrappers, load_font_faces_used_by_subtrees,
sync_document_fonts_for_handle,
};
use css_state::{detached_document_fonts_getter, document_fonts_getter_function};
pub(crate) use css_state::{
@@ -14,6 +14,7 @@ pub(crate) use accessors::{
node_document_adopted_style_sheets_getter_function,
node_document_adopted_style_sheets_setter_function, node_document_style_sheets_getter_function,
};
pub(crate) use font_faces::load_font_faces_used_by_subtrees;
pub(crate) use projection::{
apply_stylesheet_owner_css_projections, apply_stylesheet_source_css_projection,
};
@@ -15,6 +15,8 @@ use crate::{
},
util::{get_private_value, serialize_v8_iter_array, set_private_value, v8_string, v8str},
};
use moli_v8_util::object_own_static_property_as_object;
use std::collections::HashSet;
use std::convert::TryFrom;
use std::rc::Rc;
@@ -164,6 +166,67 @@ pub(super) fn finish_font_face_owner_projections<'s>(
rebuild_connected_font_faces(scope, fonts, host, document);
}
pub(crate) fn load_font_faces_used_by_subtrees(
scope: &mut v8::PinScope<'_, '_>,
host: &JsContextHost,
roots: &[DomHandle],
) {
let mut visited = HashSet::new();
for root in roots.iter().copied() {
if !host.dom_host().is_connected(root) {
continue;
}
let Some(document) = host.dom_host().owner_document_handle(root) else {
continue;
};
let Some(holder) = crate::util::node_wrapper_from_handle(scope, document) else {
continue;
};
let Some(fonts) = object_own_static_property_as_object(scope, holder, FONTS_SLOT) else {
continue;
};
for family in inline_font_families_in_subtree(host, root, &mut visited) {
crate::context_bootstrap::load_font_faces_for_family(scope, fonts, &family);
}
}
}
fn inline_font_families_in_subtree(
host: &JsContextHost,
root: DomHandle,
visited: &mut HashSet<DomHandle>,
) -> Vec<String> {
let mut families = Vec::new();
let mut stack = vec![root];
while let Some(handle) = stack.pop() {
if !visited.insert(handle) {
continue;
}
if let Some(style) = host.dom_host().get_attribute(handle, "style")
&& let Some(value) =
moli_css_parse::parse_declaration_block(&style).property_value("font-family")
&& let Some(components) =
crate::css_style::top_level_comma_separated_component_values(&value)
{
for family in components {
let family = moli_css_parse::unquote_css_string(family.trim());
if !family.is_empty()
&& !families
.iter()
.any(|existing: &String| existing.eq_ignore_ascii_case(&family))
{
families.push(family);
}
}
}
if let Some(shadow_root) = host.dom_host().shadow_root_handle(handle) {
stack.push(shadow_root);
}
stack.extend(host.dom_host().child_handles(handle));
}
families
}
fn set_owner_font_face_contribution<'s>(
scope: &mut v8::PinScope<'s, '_>,
fonts: v8::Local<'s, v8::Object>,
@@ -1708,11 +1708,83 @@ fn font_face_declared_slots_ignore_prototype_spoofing() {
assert_eq!(
result,
r#"{"values":"Changed|url(demo.woff)|italic|700|condensed|small-caps|\"kern\"|swap|loaded|function","fake":"TypeError|TypeError|TypeError|TypeError|Promise","descriptors":["family:function:get family:0:function:set family:1:true:true:false","style:function:get style:0:function:set style:1:true:true:false","weight:function:get weight:0:function:set weight:1:true:true:false","stretch:function:get stretch:0:function:set stretch:1:true:true:false","variant:function:get variant:0:function:set variant:1:true:true:false","featureSettings:function:get featureSettings:0:function:set featureSettings:1:true:true:false","display:function:get display:0:function:set display:1:true:true:false","source:function:get source:0:undefined:undefined:undefined:true:true:false","status:function:get status:0:undefined:undefined:undefined:true:true:false","loaded:function:get loaded:0:undefined:undefined:undefined:true:true:false"],"ownSlots":[]}"#
r#"{"values":"Changed|url(demo.woff)|italic|700|condensed|small-caps|\"kern\"|swap|unloaded|function","fake":"TypeError|TypeError|TypeError|TypeError|Promise","descriptors":["family:function:get family:0:function:set family:1:true:true:false","style:function:get style:0:function:set style:1:true:true:false","weight:function:get weight:0:function:set weight:1:true:true:false","stretch:function:get stretch:0:function:set stretch:1:true:true:false","variant:function:get variant:0:function:set variant:1:true:true:false","featureSettings:function:get featureSettings:0:function:set featureSettings:1:true:true:false","display:function:get display:0:function:set display:1:true:true:false","source:function:get source:0:undefined:undefined:undefined:true:true:false","status:function:get status:0:undefined:undefined:undefined:true:true:false","loaded:function:get loaded:0:undefined:undefined:undefined:true:true:false"],"ownSlots":[]}"#
);
assert_eq!(vm.eval("fakeLoadedResult").unwrap(), "rejected:TypeError");
}
#[test]
fn font_face_string_sources_follow_load_state_and_connected_style_use() {
let mut vm = new_storage_test_vm("https://font-face-string-source-state.test/");
vm.eval(
r#"
(() => {
const remote = new FontFace('RemoteFace', 'url(remote.woff2)');
const remoteLoaded = remote.loaded;
const invalid = new FontFace('InvalidFace', 'not a font source');
const local = new FontFace(
'MissingLocalFace',
'local("definitely-missing-moli-font")'
);
document.fonts.add(local);
globalThis.__fontFaceStringSourceProbe = {
remote: {
before: remote.status,
stablePromise: remote.loaded === remoteLoaded,
loadReturnsLoaded: remote.load() === remoteLoaded,
after: remote.status,
settlement: 'pending'
},
invalid: {
status: invalid.status,
settlement: 'pending'
},
local: {
beforeStyle: local.status,
afterDetachedStyle: '',
afterConnection: '',
settlement: 'pending'
}
};
remoteLoaded.then(
value => {
globalThis.__fontFaceStringSourceProbe.remote.settlement =
value === remote ? 'resolved-self' : 'resolved-other';
},
error => {
globalThis.__fontFaceStringSourceProbe.remote.settlement = error.name;
}
);
invalid.loaded.then(
() => { globalThis.__fontFaceStringSourceProbe.invalid.settlement = 'resolved'; },
error => { globalThis.__fontFaceStringSourceProbe.invalid.settlement = error.name; }
);
local.loaded.then(
() => { globalThis.__fontFaceStringSourceProbe.local.settlement = 'resolved'; },
error => { globalThis.__fontFaceStringSourceProbe.local.settlement = error.name; }
);
const target = document.createElement('div');
target.style.fontFamily = 'MissingLocalFace';
globalThis.__fontFaceStringSourceProbe.local.afterDetachedStyle = local.status;
(document.body || document.documentElement || document).appendChild(target);
globalThis.__fontFaceStringSourceProbe.local.afterConnection = local.status;
})()
"#,
)
.expect("string-backed FontFace state probe should initialize");
let result = vm
.eval("JSON.stringify(globalThis.__fontFaceStringSourceProbe)")
.expect("string-backed FontFace promises should settle");
assert_eq!(
result,
r#"{"remote":{"before":"unloaded","stablePromise":true,"loadReturnsLoaded":true,"after":"loaded","settlement":"resolved-self"},"invalid":{"status":"error","settlement":"SyntaxError"},"local":{"beforeStyle":"unloaded","afterDetachedStyle":"unloaded","afterConnection":"error","settlement":"NetworkError"}}"#
);
}
#[test]
fn font_face_variation_settings_use_stylo_descriptor_serialization() {
let mut vm = new_storage_test_vm("https://font-face-variation-settings.test/");
@@ -34,7 +34,7 @@ test(function () {
assert_equals(face.variant, "small-caps");
assert_equals(face.featureSettings, '"liga" 0');
assert_equals(face.display, "swap");
assert_equals(face.status, "loaded");
assert_equals(face.status, "unloaded");
assert_true(face.loaded instanceof Promise);
assert_true(face.load() instanceof Promise);
}, "FontFace exposes constructor metadata and descriptors");
@@ -69,9 +69,10 @@ test(function () {
promise_test(async function () {
const face = new FontFace("PromiseFace", "url(promise.woff)");
const loaded = face.loaded;
assert_equals(await face.loaded, face);
assert_equals(await face.load(), face);
assert_equals(await loaded, face);
}, "FontFace loaded and load promises resolve with the face");
promise_test(async function () {
@@ -87,7 +87,7 @@ promise_test(async function () {
assert_equals(fonts.ready, fonts.ready);
assert_equals(face.family, "FixtureFace");
assert_equals(face.source, "url(fixture.woff2)");
assert_equals(face.status, "loaded");
assert_equals(face.status, "unloaded");
assert_equals(await face.load(), face);
assert_equals(await face.loaded, face);