mirror of
https://github.com/lexmount/moli.git
synced 2026-10-30 08:07:38 +00:00
fix(fonts): defer set events and preserve ranges and promises
This commit is contained in:
+3
-2
@@ -30,12 +30,13 @@
|
||||
throw new Error("font handler boom");
|
||||
};
|
||||
try {
|
||||
document.fonts.load("italic bold 16px Roboto");
|
||||
document.fonts.load("italic bold 16px Roboto")
|
||||
.then(() => document.fonts.ready)
|
||||
.then(() => body.setAttribute("data-font-order", fontOrder.join("|")));
|
||||
body.setAttribute("data-font-continued", "true");
|
||||
} catch (error) {
|
||||
body.setAttribute("data-font-error", String(error && error.message || error));
|
||||
}
|
||||
body.setAttribute("data-font-order", fontOrder.join("|"));
|
||||
|
||||
const mql = matchMedia("(min-width: 1px)");
|
||||
const mqlOrder = [];
|
||||
|
||||
+3
@@ -11,6 +11,9 @@
|
||||
window.runtimeOwnedAsyncFastOrder.push("load:" + document.readyState);
|
||||
window.runtimeOwnedAsyncFastLoadOrder =
|
||||
window.runtimeOwnedAsyncFastOrder.join(",");
|
||||
if (window.runtimeOwnedAsyncFastTailGated) {
|
||||
fetch('/assets/runtime_owned_async_fast_executed');
|
||||
}
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
window.runtimeOwnedAsyncFastOrder.push("after-append:async=" + script.async);
|
||||
|
||||
@@ -4421,7 +4421,11 @@ async fn child_browsing_context_navigation_forward_result_promises_stay_window_l
|
||||
))
|
||||
.await?;
|
||||
browser
|
||||
.wait_for_page_delay(&mut page, Duration::from_millis(250))
|
||||
.wait_for_script_truthy(
|
||||
&mut page,
|
||||
r#"document.body.dataset.childTimeoutFinished === "true""#,
|
||||
Duration::from_secs(2),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert!(
|
||||
|
||||
@@ -854,9 +854,16 @@ async fn local_event_target_callback_exceptions_do_not_abort_dispatch() -> Resul
|
||||
let server = FixtureServer::spawn().await?;
|
||||
let browser = Browser::new(AppConfig::default())?;
|
||||
|
||||
let page = browser
|
||||
let mut page = browser
|
||||
.fetch(&server.url("/compat/local-event-target-callback-exception"))
|
||||
.await?;
|
||||
browser
|
||||
.wait_for_script_truthy(
|
||||
&mut page,
|
||||
r#"document.body.dataset.fontOrder === "first|second|handler""#,
|
||||
Duration::from_secs(3),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert!(
|
||||
page.serialize_html_async()
|
||||
@@ -2344,9 +2351,16 @@ async fn document_fonts_remove_event_listener_keeps_other_event_types_intact() -
|
||||
let server = FixtureServer::spawn().await?;
|
||||
let browser = Browser::new(AppConfig::default())?;
|
||||
|
||||
let page = browser
|
||||
let mut page = browser
|
||||
.fetch(&server.url("/compat/document-fonts-events"))
|
||||
.await?;
|
||||
browser
|
||||
.wait_for_script_truthy(
|
||||
&mut page,
|
||||
r#"document.body.dataset.loadingdone === "1""#,
|
||||
Duration::from_secs(3),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert!(
|
||||
page.serialize_html_async()
|
||||
@@ -4035,9 +4049,16 @@ async fn chrome_fontfaceset_event_handlers_fire_in_loading_then_done_order_subse
|
||||
let server = FixtureServer::spawn().await?;
|
||||
let browser = Browser::new(AppConfig::default())?;
|
||||
|
||||
let page = browser
|
||||
let mut page = browser
|
||||
.fetch(&server.url("/compat/chrome-fontfaceset-events-subset"))
|
||||
.await?;
|
||||
browser
|
||||
.wait_for_script_truthy(
|
||||
&mut page,
|
||||
r#"document.body.dataset.readyStatus === "loaded""#,
|
||||
Duration::from_secs(3),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert!(
|
||||
page.serialize_html_async()
|
||||
|
||||
@@ -700,6 +700,36 @@ fn worker_src_none_blocks_shared_worker_script() {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_font_requests_use_font_src_and_default_src() {
|
||||
let kind = ContentSecurityPolicyResourceKind::DocumentFont;
|
||||
assert!(!allowed(
|
||||
"font-src 'none'; connect-src *",
|
||||
kind,
|
||||
"https://app.test/font.woff2"
|
||||
));
|
||||
assert!(allowed(
|
||||
"font-src 'self'; default-src 'none'; connect-src 'none'",
|
||||
kind,
|
||||
"https://app.test/font.woff2"
|
||||
));
|
||||
assert!(!allowed(
|
||||
"font-src 'self'; default-src *",
|
||||
kind,
|
||||
"https://cdn.test/font.woff2"
|
||||
));
|
||||
assert!(!allowed(
|
||||
"default-src 'none'; connect-src *",
|
||||
kind,
|
||||
"https://app.test/font.woff2"
|
||||
));
|
||||
assert!(allowed(
|
||||
"default-src 'self'; connect-src 'none'",
|
||||
kind,
|
||||
"https://app.test/font.woff2"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_worker_script_uses_script_src_and_default_src_fallbacks() {
|
||||
assert!(!allowed(
|
||||
|
||||
@@ -265,6 +265,53 @@ pub(super) fn dispatch_font_face_set_event<'s>(
|
||||
const READY_RESOLVER: &str = "__moliFontFaceSetReadyResolver";
|
||||
const LOADED_FACES: &str = "__moliFontFaceSetLoadedFaces";
|
||||
const FAILED_FACES: &str = "__moliFontFaceSetFailedFaces";
|
||||
const FINISH_QUEUED: &str = "__moliFontFaceSetFinishQueued";
|
||||
|
||||
fn queue_font_face_set_task<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
owner: v8::Local<'s, v8::Object>,
|
||||
finish: bool,
|
||||
) {
|
||||
let Some(context) = owner.get_creation_context(scope) else {
|
||||
return;
|
||||
};
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
let Some(host_ptr) = crate::util::context_host_ptr_from_global_bridge(scope) else {
|
||||
return;
|
||||
};
|
||||
let data = v8::Array::new_with_elements(
|
||||
scope,
|
||||
&[owner.into(), v8::Boolean::new(scope, finish).into()],
|
||||
);
|
||||
let callback = v8::Function::builder(run_font_face_set_task)
|
||||
.data(data.into())
|
||||
.build(scope)
|
||||
.expect("font loading task");
|
||||
// The internal task scheduler captures this realm's exact Window/Document
|
||||
// lifetime and discards the callback when that owner retires.
|
||||
unsafe { &mut *host_ptr }.queue_internal_task(scope, callback);
|
||||
}
|
||||
|
||||
fn run_font_face_set_task<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
args: v8::FunctionCallbackArguments<'s>,
|
||||
_rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
let data = v8::Local::<v8::Array>::try_from(args.data()).expect("font loading task data");
|
||||
let owner = data
|
||||
.get_index(scope, 0)
|
||||
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
|
||||
.expect("font loading task owner");
|
||||
if data
|
||||
.get_index(scope, 1)
|
||||
.is_some_and(|value| value.is_true())
|
||||
{
|
||||
set_private_value(scope, owner, FINISH_QUEUED, v8::undefined(scope).into());
|
||||
finish_font_set_task(scope, owner);
|
||||
} else {
|
||||
let _ = dispatch_font_face_set_event(scope, owner, "loading", None);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn font_face_loading_started<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
@@ -297,7 +344,7 @@ pub(super) fn font_face_set_loading_started<'s>(
|
||||
let array = v8::Array::new(scope, 0);
|
||||
set_private_value(scope, owner, slot, array.into());
|
||||
}
|
||||
let _ = dispatch_font_face_set_event(scope, owner, "loading", None);
|
||||
queue_font_face_set_task(scope, owner, false);
|
||||
}
|
||||
|
||||
pub(super) fn notify_font_face_set_owners_of_load<'s>(
|
||||
@@ -340,13 +387,31 @@ pub(super) fn finish_font_set_if_idle<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
owner: v8::Local<'s, v8::Object>,
|
||||
) {
|
||||
if super::loading::string_slot(scope, owner, FONT_FACE_SET_STATUS_SLOT) != "loading" {
|
||||
if !font_set_is_loading_and_idle(scope, owner)
|
||||
|| get_private_value(scope, owner, FINISH_QUEUED).is_some_and(|value| value.is_true())
|
||||
{
|
||||
return;
|
||||
}
|
||||
set_private_value(
|
||||
scope,
|
||||
owner,
|
||||
FINISH_QUEUED,
|
||||
v8::Boolean::new(scope, true).into(),
|
||||
);
|
||||
queue_font_face_set_task(scope, owner, true);
|
||||
}
|
||||
|
||||
fn font_set_is_loading_and_idle<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
owner: v8::Local<'s, v8::Object>,
|
||||
) -> bool {
|
||||
if super::loading::string_slot(scope, owner, FONT_FACE_SET_STATUS_SLOT) != "loading" {
|
||||
return false;
|
||||
}
|
||||
// Native CSS requests can be pending before a JS wrapper is projected.
|
||||
// Conversely, the native CSS queue does not contain JS-created faces.
|
||||
if super::storage::document_font_face_set_has_pending_requests(scope, owner) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
let pending = super::storage::font_face_set_faces_array(scope, owner).is_some_and(|faces| {
|
||||
(0..faces.length()).any(|index| {
|
||||
@@ -359,7 +424,13 @@ pub(super) fn finish_font_set_if_idle<'s>(
|
||||
})
|
||||
})
|
||||
});
|
||||
if pending {
|
||||
!pending
|
||||
}
|
||||
|
||||
fn finish_font_set_task<'s>(scope: &mut v8::PinScope<'s, '_>, owner: v8::Local<'s, v8::Object>) {
|
||||
// Another font may have started between queuing this task and running it.
|
||||
// Keep its ready promise pending and let its completion queue a new task.
|
||||
if !font_set_is_loading_and_idle(scope, owner) {
|
||||
return;
|
||||
}
|
||||
let events = [
|
||||
@@ -369,6 +440,7 @@ pub(super) fn finish_font_set_if_idle<'s>(
|
||||
.map(|(slot, event)| {
|
||||
let faces = get_private_value(scope, owner, slot)
|
||||
.and_then(|v| v8::Local::<v8::Array>::try_from(v).ok());
|
||||
set_private_value(scope, owner, slot, v8::undefined(scope).into());
|
||||
(faces, event)
|
||||
});
|
||||
set_font_face_set_status(scope, owner, "loaded");
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::font_loading::{FontFaceLoad, FontFaceResource, FontFaceStatus};
|
||||
use crate::util::{get_private_value, set_private_value};
|
||||
|
||||
const RESOLVER: &str = "__moliFontFaceResolver";
|
||||
const PROMISE_OWNER: &str = "__moliFontFacePromiseOwner";
|
||||
const OWNER_DOCUMENT: &str = "__moliFontFaceSetOwnerDocument";
|
||||
static NEXT_REGISTRATION: AtomicU64 = AtomicU64::new(1);
|
||||
const QUERY_RESOLVER: &str = "__moliFontQueryResolver";
|
||||
@@ -147,6 +148,7 @@ fn descriptor<'s>(
|
||||
&string_slot(scope, face, FONT_FACE_WEIGHT_SLOT),
|
||||
&string_slot(scope, face, FONT_FACE_STRETCH_SLOT),
|
||||
&string_slot(scope, face, FONT_FACE_STYLE_SLOT),
|
||||
&string_slot(scope, face, FONT_FACE_UNICODE_RANGE_SLOT),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -166,6 +168,16 @@ pub(super) fn initialize_resolver<'s>(
|
||||
resource: FontFaceLoad,
|
||||
) {
|
||||
set_private_value(scope, face, RESOLVER, resolver.into());
|
||||
// A retained pending .loaded promise must retain its settlement endpoint,
|
||||
// including CSS-started loads with no explicit JS network request root.
|
||||
// Keep this edge in the JS heap so an otherwise unreachable cycle remains
|
||||
// collectible; no isolate-wide Global may keep a retired document alive.
|
||||
set_private_value(
|
||||
scope,
|
||||
resolver.get_promise(scope).into(),
|
||||
PROMISE_OWNER,
|
||||
face.into(),
|
||||
);
|
||||
super::state::initialize(scope, face, resource);
|
||||
publish_state(scope, face);
|
||||
}
|
||||
@@ -187,6 +199,7 @@ fn publish_state<'s>(scope: &mut v8::PinScope<'s, '_>, face: v8::Local<'s, v8::O
|
||||
FontFaceStatus::Loaded => {
|
||||
sync_owners(scope, face);
|
||||
if let Some(resolver) = resolver_from_slot(scope, face, RESOLVER) {
|
||||
release_promise_owner(scope, face, resolver);
|
||||
let _ = resolver.resolve(scope, face.into());
|
||||
}
|
||||
}
|
||||
@@ -194,6 +207,7 @@ fn publish_state<'s>(scope: &mut v8::PinScope<'s, '_>, face: v8::Local<'s, v8::O
|
||||
if let Some(resolver) = resolver_from_slot(scope, face, RESOLVER) {
|
||||
let error = error.expect("native failed font has an error");
|
||||
let exception = new_dom_exception_value(scope, error.message, error.name);
|
||||
release_promise_owner(scope, face, resolver);
|
||||
let _ = resolver.reject(scope, exception);
|
||||
}
|
||||
}
|
||||
@@ -204,6 +218,20 @@ fn publish_state<'s>(scope: &mut v8::PinScope<'s, '_>, face: v8::Local<'s, v8::O
|
||||
}
|
||||
}
|
||||
|
||||
fn release_promise_owner<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
face: v8::Local<'s, v8::Object>,
|
||||
resolver: v8::Local<'s, v8::PromiseResolver>,
|
||||
) {
|
||||
set_private_value(
|
||||
scope,
|
||||
resolver.get_promise(scope).into(),
|
||||
PROMISE_OWNER,
|
||||
v8::undefined(scope).into(),
|
||||
);
|
||||
set_private_value(scope, face, RESOLVER, v8::undefined(scope).into());
|
||||
}
|
||||
|
||||
pub(crate) fn publish_font_face_load_changes(scope: &mut v8::PinScope<'_, '_>) {
|
||||
for face in super::state::changed_wrappers(scope) {
|
||||
if let Some(context) = face.get_creation_context(scope) {
|
||||
@@ -307,14 +335,25 @@ pub(super) fn sync_registration<'s>(
|
||||
// SAFETY: this is the owner document's live bridge; the native font service
|
||||
// does not invoke V8 or page code while borrowed.
|
||||
let host = unsafe { &*host_ptr };
|
||||
host.with_font_services(document, |services| {
|
||||
let changed = host.with_font_services(document, |services| {
|
||||
let load = load.borrow();
|
||||
if present {
|
||||
if let Err(error) = load.register(services) {
|
||||
tracing::warn!(%error, "failed to register FontFace");
|
||||
match load.register(services) {
|
||||
Ok(Some(
|
||||
moli_layout::WebFontRegistrationOutcome::Added
|
||||
| moli_layout::WebFontRegistrationOutcome::Replaced,
|
||||
)) => true,
|
||||
Ok(_) => false,
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, "failed to register FontFace");
|
||||
false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
load.unregister(services);
|
||||
load.unregister(services)
|
||||
}
|
||||
});
|
||||
if changed {
|
||||
host.invalidate_document_font_layout(document);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,9 +248,11 @@ pub(crate) fn rebuild_font_face_set_faces<'s>(
|
||||
}
|
||||
let combined =
|
||||
serialize_v8_iter_array(scope, combined).unwrap_or_else(|| v8::Array::new(scope, 0));
|
||||
sync_font_face_set_owners(scope, object, previous, combined);
|
||||
set_font_face_set_slot_value(scope, object, FONT_FACE_SET_FACES_SLOT, combined.into());
|
||||
sync_font_face_set_size(scope, object);
|
||||
// Publish membership before changing owners can start a loading cycle.
|
||||
// Reentrant FontFaceSet queries must see the same members as that cycle.
|
||||
sync_font_face_set_owners(scope, object, previous, combined);
|
||||
super::events::finish_font_set_if_idle(scope, object);
|
||||
}
|
||||
|
||||
|
||||
@@ -301,7 +301,7 @@ fn parsed_web_font_face(css_text: &str) -> Option<ParsedWebFontFace> {
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_font_unicode_ranges(value: &str) -> Option<Vec<WebFontUnicodeRange>> {
|
||||
pub(crate) fn parse_font_unicode_ranges(value: &str) -> Option<Vec<WebFontUnicodeRange>> {
|
||||
let input = value;
|
||||
let mut input = Parser::new(input);
|
||||
input
|
||||
|
||||
@@ -19,9 +19,11 @@ pub(crate) fn script_font_descriptor(
|
||||
weight: &str,
|
||||
stretch: &str,
|
||||
style: &str,
|
||||
unicode_range: &str,
|
||||
) -> WebFontFace {
|
||||
use crate::css_resource_urls::{
|
||||
parse_font_stretch_lower_bound, parse_font_style_lower_bound, parse_font_weight_lower_bound,
|
||||
parse_font_stretch_lower_bound, parse_font_style_lower_bound, parse_font_unicode_ranges,
|
||||
parse_font_weight_lower_bound,
|
||||
};
|
||||
WebFontFace::new(moli_css_parse::unquote_css_string(family))
|
||||
.with_weight(parse_font_weight_lower_bound(weight).unwrap_or(400.0))
|
||||
@@ -29,6 +31,7 @@ pub(crate) fn script_font_descriptor(
|
||||
.with_style(
|
||||
parse_font_style_lower_bound(style).unwrap_or(moli_layout::WebFontStyle::Normal),
|
||||
)
|
||||
.with_unicode_ranges(parse_font_unicode_ranges(unicode_range).unwrap_or_default())
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@@ -96,7 +99,7 @@ impl FontFaceResource {
|
||||
}
|
||||
|
||||
pub(crate) fn set_descriptor(&mut self, descriptor: WebFontFace) {
|
||||
self.descriptor = descriptor.with_unicode_ranges(self.descriptor.unicode_ranges().to_vec());
|
||||
self.descriptor = descriptor;
|
||||
}
|
||||
|
||||
pub(crate) fn status(&self) -> FontFaceStatus {
|
||||
@@ -195,7 +198,7 @@ impl FontFaceResource {
|
||||
services.register_web_font(registration).map(Some)
|
||||
}
|
||||
|
||||
pub(crate) fn unregister(&self, services: &mut DocumentLayoutServices) {
|
||||
services.remove_web_font(&self.slot);
|
||||
pub(crate) fn unregister(&self, services: &mut DocumentLayoutServices) -> bool {
|
||||
services.remove_web_font(&self.slot)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,6 +317,22 @@ impl JsContextHost {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn invalidate_document_font_layout(&self, document: DomHandle) {
|
||||
if document != self.document_handle()
|
||||
&& self
|
||||
.child_browsing_context_host_for_document_handle(document)
|
||||
.is_none()
|
||||
{
|
||||
return;
|
||||
}
|
||||
// A real font registration change invalidates sampled glyph advances.
|
||||
// Unchanged registration and read-only projections retain the tree.
|
||||
self.clear_layout_rect_cache();
|
||||
self.document_layout_state
|
||||
.borrow_mut()
|
||||
.clear_latest_layout();
|
||||
}
|
||||
|
||||
pub(crate) fn build_layout_pass_for_document(
|
||||
&self,
|
||||
document: DomHandle,
|
||||
|
||||
@@ -105,6 +105,7 @@ fn construct_font_face_from_parts<'s>(
|
||||
"font-feature-settings" => "featureSettings",
|
||||
"font-variation-settings" => "variationSettings",
|
||||
"font-display" => "display",
|
||||
"unicode-range" => "unicodeRange",
|
||||
_ => continue,
|
||||
};
|
||||
let value = v8_string(scope, &entry.value)?;
|
||||
|
||||
@@ -3,6 +3,7 @@ use base64::Engine;
|
||||
use std::time::Duration;
|
||||
|
||||
mod readiness;
|
||||
mod unicode_ranges;
|
||||
|
||||
pub(super) fn fixture_font_url() -> String {
|
||||
let bytes = include_bytes!(concat!(
|
||||
@@ -180,6 +181,7 @@ document.fonts.load('12px "Network Face"').then(faces => fontResult.push(faces[0
|
||||
.unwrap();
|
||||
vm.finish_async_subresource_body_checkpoint_for_test(activity)
|
||||
.unwrap();
|
||||
run_font_tasks(&mut vm);
|
||||
assert_eq!(
|
||||
vm.eval("JSON.stringify(fontResult)").unwrap(),
|
||||
"[true,\"loaded\"]"
|
||||
@@ -273,6 +275,7 @@ fontPromise.then(value => fontResult.push(value === face, face.status));
|
||||
.unwrap();
|
||||
vm.finish_async_subresource_body_checkpoint_for_test(activity)
|
||||
.unwrap();
|
||||
run_font_tasks(&mut vm);
|
||||
assert_eq!(
|
||||
vm.eval("JSON.stringify(fontResult)").unwrap(),
|
||||
r#"[true,"loaded"]"#
|
||||
|
||||
@@ -11,6 +11,25 @@ async fn gated_fonts(
|
||||
tokio::sync::mpsc::UnboundedReceiver<String>,
|
||||
FontGates,
|
||||
tokio::task::JoinHandle<()>,
|
||||
) {
|
||||
gated_fonts_with_bytes(
|
||||
names,
|
||||
include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../moli-layout/tests/fixtures/moli-ahem.woff2"
|
||||
)),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn gated_fonts_with_bytes(
|
||||
names: &[&str],
|
||||
font: &'static [u8],
|
||||
) -> (
|
||||
String,
|
||||
tokio::sync::mpsc::UnboundedReceiver<String>,
|
||||
FontGates,
|
||||
tokio::task::JoinHandle<()>,
|
||||
) {
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let origin = format!("http://{}", listener.local_addr().unwrap());
|
||||
@@ -39,10 +58,6 @@ async fn gated_fonts(
|
||||
requests_tx.send(path).unwrap();
|
||||
responses.push(tokio::spawn(async move {
|
||||
let _ = release.await;
|
||||
let font = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../moli-layout/tests/fixtures/moli-ahem.woff2"
|
||||
));
|
||||
let head = format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: font/woff2\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
||||
font.len()
|
||||
@@ -121,6 +136,7 @@ async fn assert_mixed_ready(js_starts_first: bool, js_finishes_first: bool) {
|
||||
.unwrap();
|
||||
vm.finish_async_subresource_body_checkpoint_for_test(activity)
|
||||
.unwrap();
|
||||
run_font_tasks(&mut vm);
|
||||
let expected = if index == 1 {
|
||||
r#"["loaded","loaded",["css:true","js:true"],["AuditCSS,AuditJS"]]"#
|
||||
} else if js_finishes_first {
|
||||
@@ -202,6 +218,7 @@ async fn font_set_ready_settles_css_started_by_explicit_font_face_load() {
|
||||
.unwrap();
|
||||
vm.finish_async_subresource_body_checkpoint_for_test(activity)
|
||||
.unwrap();
|
||||
run_font_tasks(&mut vm);
|
||||
assert_eq!(
|
||||
vm.eval("JSON.stringify([face.status,readyResolved])")
|
||||
.unwrap(),
|
||||
@@ -253,3 +270,368 @@ async fn font_face_url_is_frozen_before_base_href_changes() {
|
||||
r#"["loaded",true]"#
|
||||
);
|
||||
}
|
||||
|
||||
async fn assert_loaded_promise_lifetime(mode: &str) {
|
||||
let failure = mode == "css-removed-error";
|
||||
let (origin, mut requests, mut gates, server) = if failure {
|
||||
gated_fonts_with_bytes(&["lifetime.woff2"], b"invalid font bytes").await
|
||||
} else {
|
||||
gated_fonts(&["lifetime.woff2"]).await
|
||||
};
|
||||
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap();
|
||||
loader.set_optional_resource_fetch_mask(crate::protocol_types::OptionalResourceFetchMask::FONT);
|
||||
let (mut vm, mut completions) = new_parsed_test_vm_with_loader_and_resource_completion_queue(
|
||||
&format!("{origin}/page"),
|
||||
"<!doctype html><html><head></head><body>AAAA</body></html>",
|
||||
&loader,
|
||||
);
|
||||
vm.set_layout_policy(moli_page_types::LayoutPolicy::OnDemand);
|
||||
if mode == "js-only" {
|
||||
vm.eval(&format!(
|
||||
r#"globalThis.promiseResults=[];
|
||||
globalThis.retainedPromise=(() => {{
|
||||
const face=new FontFace('Lifetime','url("{origin}/lifetime.woff2")');
|
||||
globalThis.faceWitness=new WeakRef(face);
|
||||
return face.load();
|
||||
}})();
|
||||
retainedPromise.then(value=>promiseResults.push([value.family,value.status]),
|
||||
error=>promiseResults.push(['error',error.name]));"#
|
||||
))
|
||||
.unwrap();
|
||||
} else {
|
||||
vm.eval(&format!(
|
||||
r#"const style=document.createElement('style');
|
||||
style.textContent='@font-face{{font-family:Lifetime;src:url("{origin}/lifetime.woff2")}}';
|
||||
document.head.append(style);document.body.style.fontFamily='Lifetime';"#
|
||||
)).unwrap();
|
||||
// Only native CSS/layout admission starts this request. Do not call
|
||||
// FontFace.load(), which installs a separate strong request root.
|
||||
vm.refresh_layout_snapshot_for_test(moli_layout::LayoutViewport::new(800, 600, 1.0))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
vm.eval(
|
||||
r#"globalThis.promiseResults=[];
|
||||
globalThis.retainedPromise=(() => {
|
||||
const face=[...document.fonts][0];
|
||||
if(face.status!=='loading') throw new Error('CSS must already be loading');
|
||||
globalThis.faceWitness=new WeakRef(face);
|
||||
return face.loaded;
|
||||
})();
|
||||
retainedPromise.then(value=>promiseResults.push([value.family,value.status]),
|
||||
error=>promiseResults.push(['error',error.name]));
|
||||
document.fonts.size"#
|
||||
)
|
||||
.unwrap(),
|
||||
"1"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
tokio::time::timeout(Duration::from_secs(2), requests.recv())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
"/lifetime.woff2"
|
||||
);
|
||||
if mode.starts_with("css-removed") {
|
||||
assert_eq!(
|
||||
vm.eval("document.styleSheets[0].deleteRule(0);document.fonts.size")
|
||||
.unwrap(),
|
||||
"0"
|
||||
);
|
||||
vm.refresh_layout_snapshot_for_test(moli_layout::LayoutViewport::new(800, 600, 1.0))
|
||||
.unwrap();
|
||||
assert_eq!(vm.document_web_font_counts_for_test(), (0, 0, 0));
|
||||
}
|
||||
assert_eq!(vm.eval("JSON.stringify(promiseResults)").unwrap(), "[]");
|
||||
assert_eq!(
|
||||
vm._context_host
|
||||
.borrow()
|
||||
.pending_subresource_request_count(),
|
||||
1
|
||||
);
|
||||
// Every vm.eval HandleScope above has exited. Clear WeakRef's job-local
|
||||
// keepalive before forcing full GC so the witness cannot root the face.
|
||||
if mode != "css-removed-no-gc" {
|
||||
vm.renderer_document_isolate
|
||||
.clone()
|
||||
.with_entered_renderer_document_isolate(|isolate| {
|
||||
isolate.clear_kept_objects();
|
||||
isolate.low_memory_notification();
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
assert_eq!(
|
||||
vm.eval("faceWitness.deref()===undefined").unwrap(),
|
||||
"false",
|
||||
"a retained .loaded promise must keep the loading wrapper alive"
|
||||
);
|
||||
gates.remove("lifetime.woff2").unwrap().send(()).unwrap();
|
||||
server.await.unwrap();
|
||||
assert!(
|
||||
tokio::time::timeout(
|
||||
Duration::from_secs(2),
|
||||
completions.wait_for_arrival_without_timeout()
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
);
|
||||
let completion = completions.pop_next_async_subresource_event().unwrap();
|
||||
let activity = vm
|
||||
.complete_async_subresource_fetch_event_body(completion)
|
||||
.unwrap();
|
||||
vm.finish_async_subresource_body_checkpoint_for_test(activity)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
vm._context_host
|
||||
.borrow()
|
||||
.pending_subresource_request_count(),
|
||||
0
|
||||
);
|
||||
let results = vm.eval("JSON.stringify(promiseResults)").unwrap();
|
||||
assert_eq!(
|
||||
results,
|
||||
if failure {
|
||||
r#"[["error","NetworkError"]]"#
|
||||
} else {
|
||||
r#"[["Lifetime","loaded"]]"#
|
||||
},
|
||||
"{mode}: retaining .loaded must preserve its settlement endpoint"
|
||||
);
|
||||
// A fulfilled promise necessarily retains its FontFace result. A rejected
|
||||
// promise must release the wrapper even while that promise remains live.
|
||||
if !failure {
|
||||
vm.eval("globalThis.retainedPromise=null").unwrap();
|
||||
}
|
||||
vm.eval("if(document.styleSheets.length&&document.styleSheets[0].cssRules.length) document.styleSheets[0].deleteRule(0); void document.fonts.size").unwrap();
|
||||
run_font_tasks(&mut vm);
|
||||
vm.renderer_document_isolate
|
||||
.clone()
|
||||
.with_entered_renderer_document_isolate(|isolate| {
|
||||
isolate.clear_kept_objects();
|
||||
isolate.low_memory_notification();
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
vm.eval("faceWitness.deref()===undefined").unwrap(),
|
||||
"true",
|
||||
"completed font wrappers must not be kept alive by their resolver"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn css_removed_loaded_promise_survives_gc() {
|
||||
assert_loaded_promise_lifetime("css-removed-gc").await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn css_removed_loaded_promise_without_forced_gc() {
|
||||
assert_loaded_promise_lifetime("css-removed-no-gc").await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn css_member_loaded_promise_survives_gc() {
|
||||
assert_loaded_promise_lifetime("css-member").await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn js_only_loaded_promise_survives_gc() {
|
||||
assert_loaded_promise_lifetime("js-only").await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn loading_handler_ready_cannot_finish_load_before_add() {
|
||||
let (origin, mut requests, mut gates, server) = gated_fonts(&["slow.woff2"]).await;
|
||||
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap();
|
||||
let (mut vm, mut completions) = new_parsed_test_vm_with_loader_and_resource_completion_queue(
|
||||
&format!("{origin}/page"),
|
||||
"<!doctype html><html><head></head><body></body></html>",
|
||||
&loader,
|
||||
);
|
||||
vm.eval(&format!(r#"globalThis.fonts=document.fonts;
|
||||
globalThis.face=new FontFace('Slow','url("{origin}/slow.woff2")');
|
||||
globalThis.readyResolved=false;globalThis.doneEvents=[];globalThis.loadingEvents=0;
|
||||
fonts.addEventListener('loading',()=>{{loadingEvents++;fonts.ready.then(()=>readyResolved=true)}});
|
||||
fonts.addEventListener('loadingdone',event=>doneEvents.push(event.fontfaces.length));
|
||||
face.load();fonts.add(face);"#)).unwrap();
|
||||
assert_eq!(
|
||||
tokio::time::timeout(Duration::from_secs(2), requests.recv())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
"/slow.woff2"
|
||||
);
|
||||
assert_eq!(
|
||||
vm.eval("loadingEvents").unwrap(),
|
||||
"0",
|
||||
"loading must run as a task after add returns"
|
||||
);
|
||||
run_font_tasks(&mut vm);
|
||||
assert_eq!(vm.eval("loadingEvents").unwrap(), "1");
|
||||
let blocked = vm
|
||||
.eval("JSON.stringify([face.status,fonts.status,readyResolved,doneEvents])")
|
||||
.unwrap();
|
||||
gates.remove("slow.woff2").unwrap().send(()).unwrap();
|
||||
server.await.unwrap();
|
||||
assert!(
|
||||
tokio::time::timeout(
|
||||
Duration::from_secs(2),
|
||||
completions.wait_for_arrival_without_timeout()
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
);
|
||||
let completion = completions.pop_next_async_subresource_event().unwrap();
|
||||
let activity = vm
|
||||
.complete_async_subresource_fetch_event_body(completion)
|
||||
.unwrap();
|
||||
vm.finish_async_subresource_body_checkpoint_for_test(activity)
|
||||
.unwrap();
|
||||
run_font_tasks(&mut vm);
|
||||
assert_eq!(
|
||||
vm.eval("JSON.stringify([face.status,readyResolved,doneEvents])")
|
||||
.unwrap(),
|
||||
r#"["loaded",true,[1]]"#
|
||||
);
|
||||
assert_eq!(blocked, r#"["loading","loading",false,[]]"#);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn css_removed_loaded_promise_rejects_after_gc_and_releases_wrapper() {
|
||||
assert_loaded_promise_lifetime("css-removed-error").await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn font_set_finish_task_rechecks_fonts_started_before_dispatch() {
|
||||
let (origin, mut requests, mut gates, server) = gated_fonts(&["slow.woff2"]).await;
|
||||
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap();
|
||||
let (mut vm, mut completions) = new_parsed_test_vm_with_loader_and_resource_completion_queue(
|
||||
&format!("{origin}/page"),
|
||||
"<!doctype html><html><head></head><body></body></html>",
|
||||
&loader,
|
||||
);
|
||||
let immediate = fixture_font_url();
|
||||
vm.eval(&format!(r#"
|
||||
const fonts=document.fonts;
|
||||
const first=new FontFace('First','url("{immediate}")');
|
||||
fonts.add(first);first.load();
|
||||
globalThis.firstReady=fonts.ready;globalThis.readyResolved=false;
|
||||
firstReady.then(()=>readyResolved=true);
|
||||
globalThis.doneEvents=[];
|
||||
fonts.addEventListener('loadingdone',event=>doneEvents.push(event.fontfaces.map(face=>face.family).sort()));
|
||||
globalThis.slow=new FontFace('Slow','url("{origin}/slow.woff2")');
|
||||
fonts.add(slow);slow.load();
|
||||
"#)).unwrap();
|
||||
assert_eq!(
|
||||
tokio::time::timeout(Duration::from_secs(2), requests.recv())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
"/slow.woff2"
|
||||
);
|
||||
run_font_tasks(&mut vm);
|
||||
assert_eq!(vm.eval("JSON.stringify([slow.status,document.fonts.status,firstReady===document.fonts.ready,readyResolved,doneEvents])").unwrap(), r#"["loading","loading",true,false,[]]"#);
|
||||
gates.remove("slow.woff2").unwrap().send(()).unwrap();
|
||||
server.await.unwrap();
|
||||
assert!(
|
||||
tokio::time::timeout(
|
||||
Duration::from_secs(2),
|
||||
completions.wait_for_arrival_without_timeout()
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
);
|
||||
let completion = completions.pop_next_async_subresource_event().unwrap();
|
||||
let activity = vm
|
||||
.complete_async_subresource_fetch_event_body(completion)
|
||||
.unwrap();
|
||||
vm.finish_async_subresource_body_checkpoint_for_test(activity)
|
||||
.unwrap();
|
||||
run_font_tasks(&mut vm);
|
||||
assert_eq!(
|
||||
vm.eval("JSON.stringify([readyResolved,doneEvents])")
|
||||
.unwrap(),
|
||||
r#"[true,[["First","Slow"]]]"#
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn font_loading_tasks_preserve_order_and_reentrant_cycle_promises() {
|
||||
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap();
|
||||
let mut page = new_storage_page_task_executor_test_vm_with_loader(
|
||||
"https://font-event-tasks.test/",
|
||||
&loader,
|
||||
);
|
||||
let source = fixture_font_url();
|
||||
page.eval(&format!(r#"
|
||||
globalThis.fontTrace=[];globalThis.secondCycle=false;
|
||||
const fonts=document.fonts;
|
||||
fonts.addEventListener('loading',()=>fontTrace.push('loading'));
|
||||
fonts.addEventListener('loadingerror',event=>fontTrace.push('error:'+event.fontfaces.map(face=>face.family).join(',')));
|
||||
fonts.addEventListener('loadingdone',event=>{{
|
||||
fontTrace.push('done:'+event.fontfaces.map(face=>face.family).join(','));
|
||||
if(!secondCycle) {{
|
||||
secondCycle=true;fonts.delete(first);
|
||||
const next=new FontFace('Next','url("{source}")');
|
||||
fonts.add(next);next.load();
|
||||
const nextReady=fonts.ready;
|
||||
fontTrace.push('new-cycle:'+(nextReady!==firstReady)+':'+fonts.status);
|
||||
nextReady.then(()=>fontTrace.push('next-ready'));
|
||||
}}
|
||||
}});
|
||||
const first=new FontFace('Failed','local("MoliDefinitelyNotAFont-009943")');
|
||||
fonts.add(first);first.load().catch(()=>{{}});
|
||||
const firstReady=fonts.ready;
|
||||
firstReady.then(()=>fontTrace.push('first-ready'));
|
||||
fontTrace.push('script');queueMicrotask(()=>fontTrace.push('microtask'));
|
||||
"#)).unwrap();
|
||||
assert_eq!(
|
||||
page.eval("JSON.stringify(fontTrace)").unwrap(),
|
||||
r#"["script","microtask"]"#
|
||||
);
|
||||
advance_page_task_executor_until_eval_equals(
|
||||
&mut page,
|
||||
&loader,
|
||||
"fontTrace.includes('next-ready')",
|
||||
"true",
|
||||
"font loading tasks should complete both cycles through the Page dispatcher",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
page.eval("JSON.stringify(fontTrace)").unwrap(),
|
||||
r#"["script","microtask","loading","done:","new-cycle:true:loading","error:Failed","first-ready","loading","done:Next","next-ready"]"#
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn font_loading_tasks_do_not_dispatch_after_their_window_retires() {
|
||||
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).unwrap();
|
||||
let mut page = new_storage_page_task_executor_test_vm_with_loader(
|
||||
"https://font-event-retirement.test/",
|
||||
&loader,
|
||||
);
|
||||
let source = fixture_font_url();
|
||||
page.eval(&format!(
|
||||
r#"
|
||||
globalThis.fontTrace=[];globalThis.fontTasksDrained=false;
|
||||
const frame=document.body.appendChild(document.createElement('iframe'));
|
||||
const child=frame.contentWindow,fonts=child.document.fonts;
|
||||
for(const type of ['loading','loadingdone','loadingerror'])
|
||||
fonts.addEventListener(type,()=>fontTrace.push(type));
|
||||
const face=new child.FontFace('Retired','url("{source}")');
|
||||
fonts.add(face);face.load();frame.remove();
|
||||
setTimeout(()=>fontTasksDrained=true,0);
|
||||
"#
|
||||
))
|
||||
.unwrap();
|
||||
advance_page_task_executor_until_eval_equals(
|
||||
&mut page,
|
||||
&loader,
|
||||
"fontTasksDrained",
|
||||
"true",
|
||||
"the surviving Window timer should run after retired font tasks are discarded",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(page.eval("JSON.stringify(fontTrace)").unwrap(), "[]");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
use super::*;
|
||||
fn font_widths(vm: &mut StandaloneScriptVmHarness) -> [f32; 6] {
|
||||
// Use the page API so this warms the same frozen geometry cache as CDP.
|
||||
// Building a fresh native pass here would hide missing font invalidation.
|
||||
let widths = vm
|
||||
.eval(
|
||||
r#"JSON.stringify(
|
||||
['rangeA','rangeB','allA','allB','fallbackA','fallbackB'].map(
|
||||
id=>document.getElementById(id).getBoundingClientRect().width))"#,
|
||||
)
|
||||
.unwrap();
|
||||
serde_json::from_str(&widths).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn font_face_unicode_range_limits_layout_and_updates_after_loading() {
|
||||
let mut vm = new_parsed_test_vm(
|
||||
"https://font-unicode-range.test/",
|
||||
"<!doctype html><html><head></head><body></body></html>",
|
||||
);
|
||||
vm.set_layout_policy(moli_page_types::LayoutPolicy::OnDemand);
|
||||
let font = base64::engine::general_purpose::STANDARD.encode(include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../moli-layout/tests/fixtures/moli-ahem.woff2"
|
||||
)));
|
||||
vm.eval(&format!(
|
||||
r#"
|
||||
const bytes=Uint8Array.from(atob('{font}'),c=>c.charCodeAt(0));
|
||||
globalThis.face=new FontFace('RangeFace',bytes,{{unicodeRange:'U+0041'}});
|
||||
const all=new FontFace('AllFace',bytes);
|
||||
document.fonts.add(face);document.fonts.add(all);
|
||||
for(const [id,family,text] of [
|
||||
['rangeA','RangeFace,monospace','AAAA'],['rangeB','RangeFace,monospace','BBBB'],
|
||||
['allA','AllFace,monospace','AAAA'],['allB','AllFace,monospace','BBBB'],
|
||||
['fallbackA','monospace','AAAA'],['fallbackB','monospace','BBBB']
|
||||
]) {{
|
||||
const span=document.createElement('span');span.id=id;span.textContent=text;
|
||||
span.style.cssText=`display:inline-block;font-size:40px;font-family:${{family}}`;
|
||||
document.body.append(span);
|
||||
}}
|
||||
"#
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(vm.eval("face.status").unwrap(), "loaded");
|
||||
let [range_a, range_b, all_a, all_b, fallback_a, fallback_b] = font_widths(&mut vm);
|
||||
assert!(
|
||||
(all_a - fallback_a).abs() > 0.1,
|
||||
"A control must distinguish the custom font from fallback"
|
||||
);
|
||||
assert!(
|
||||
(all_b - fallback_b).abs() > 0.1,
|
||||
"B control must distinguish the custom font from fallback"
|
||||
);
|
||||
assert_eq!(range_a, all_a);
|
||||
assert_eq!(range_b, fallback_b);
|
||||
|
||||
vm.eval("face.unicodeRange='U+0042'").unwrap();
|
||||
let [range_a, range_b, ..] = font_widths(&mut vm);
|
||||
assert_eq!(range_a, fallback_a);
|
||||
assert_eq!(range_b, all_b);
|
||||
|
||||
// Other descriptor writes must not restore the constructor's old range.
|
||||
let generation = vm._context_host.borrow().visual_state_generation();
|
||||
vm.eval("face.weight='400'").unwrap();
|
||||
assert_eq!(
|
||||
vm._context_host.borrow().visual_state_generation(),
|
||||
generation
|
||||
);
|
||||
let [range_a, range_b, ..] = font_widths(&mut vm);
|
||||
assert_eq!(range_a, fallback_a);
|
||||
assert_eq!(range_b, all_b);
|
||||
|
||||
vm.eval("document.fonts.delete(face)").unwrap();
|
||||
let [range_a, range_b, ..] = font_widths(&mut vm);
|
||||
assert_eq!(range_a, fallback_a);
|
||||
assert_eq!(range_b, fallback_b);
|
||||
vm.eval("document.fonts.add(face)").unwrap();
|
||||
let [range_a, range_b, ..] = font_widths(&mut vm);
|
||||
assert_eq!(range_a, fallback_a);
|
||||
assert_eq!(range_b, all_b);
|
||||
}
|
||||
@@ -205,9 +205,8 @@ fn font_face_set_load_event_copies_and_freezes_fontfaces() {
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let result = vm
|
||||
.eval(
|
||||
r#"
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const face = new FontFace('Demo', fixtureFontSource);
|
||||
const source = [face];
|
||||
@@ -257,7 +256,7 @@ fn font_face_set_load_event_copies_and_freezes_fontfaces() {
|
||||
FontFaceSetLoadEvent.prototype,
|
||||
'fontfaces'
|
||||
);
|
||||
return JSON.stringify({
|
||||
globalThis.__fontFaceSetEventProbe = {
|
||||
surface: [
|
||||
typeof FontFaceSetLoadEvent,
|
||||
FontFaceSetLoadEvent.length,
|
||||
@@ -294,13 +293,17 @@ fn font_face_set_load_event_copies_and_freezes_fontfaces() {
|
||||
throwsTypeError(() => new FontFaceSetLoadEvent('loading', { fontfaces: [{}] })),
|
||||
throwsTypeError(() => new FontFaceSetLoadEvent('loading', { fontfaces: null }))
|
||||
],
|
||||
dispatched: [loadingShape, doneShape]
|
||||
});
|
||||
get dispatched() { return [loadingShape, doneShape]; }
|
||||
};
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("FontFaceSetLoadEvent FrozenArray semantics should evaluate");
|
||||
)
|
||||
.expect("FontFaceSetLoadEvent FrozenArray semantics should evaluate");
|
||||
|
||||
run_font_tasks(&mut vm);
|
||||
let result = vm
|
||||
.eval("JSON.stringify(globalThis.__fontFaceSetEventProbe)")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
r#"{"surface":["function",1,true,true,"[object FontFaceSetLoadEvent]"],"empty":[true,0,true,true],"configured":["loadingdone",true,true,true,1,true,true,true],"descriptor":["function","undefined","true","true","false"],"errors":[true,true,true,true],"dispatched":["true:true:true:0","true:true:true:1:true:true"]}"#
|
||||
@@ -475,9 +478,8 @@ fn font_face_load_updates_owner_sets_once_and_unlinks_removed_faces() {
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let result = vm
|
||||
.eval(
|
||||
r#"
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const face = new FontFace('Demo', fixtureFontSource);
|
||||
const documentFonts = document.fonts;
|
||||
@@ -528,7 +530,7 @@ fn font_face_load_updates_owner_sets_once_and_unlinks_removed_faces() {
|
||||
const eventCountBeforeClearedLoad = events.length;
|
||||
cleared.load();
|
||||
|
||||
return JSON.stringify({
|
||||
globalThis.__fontFaceOwnerProbe = {
|
||||
loads: [firstLoad === face.loaded, secondLoad === face.loaded],
|
||||
ready: [
|
||||
documentReadyBefore !== documentReadyAfter,
|
||||
@@ -536,7 +538,7 @@ fn font_face_load_updates_owner_sets_once_and_unlinks_removed_faces() {
|
||||
documentReadyAfter === documentFonts.ready,
|
||||
secondaryReadyAfter === secondary.ready
|
||||
],
|
||||
status: [documentFonts.status, secondary.status],
|
||||
get status() { return [documentFonts.status, secondary.status]; },
|
||||
events,
|
||||
repeated: events.length === eventCountAfterFirstLoad,
|
||||
removed: [
|
||||
@@ -549,12 +551,16 @@ fn font_face_load_updates_owner_sets_once_and_unlinks_removed_faces() {
|
||||
clearedSet.ready === clearedReadyBeforeLoad,
|
||||
clearedSet.status === 'loaded'
|
||||
]
|
||||
});
|
||||
};
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("FontFace loads should update each current owner set once");
|
||||
)
|
||||
.expect("FontFace loads should update each current owner set once");
|
||||
|
||||
run_font_tasks(&mut vm);
|
||||
let result = vm
|
||||
.eval("JSON.stringify(globalThis.__fontFaceOwnerProbe)")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
r#"{"loads":[true,true],"ready":[true,true,true,true],"status":["loaded","loaded"],"events":["document:loading:true:true:true:0:true:true","secondary:loading:true:true:true:0:true:true","document:loadingdone:true:true:true:1:true:true","secondary:loadingdone:true:true:true:1:true:true"],"repeated":true,"removed":[true,true,true],"cleared":[true,true,true]}"#
|
||||
@@ -572,9 +578,8 @@ fn stylesheet_font_face_load_updates_document_font_set() {
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let result = vm
|
||||
.eval(
|
||||
r#"
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
const observed = [];
|
||||
let face;
|
||||
@@ -594,18 +599,22 @@ fn stylesheet_font_face_load_updates_document_font_set() {
|
||||
face = [...document.fonts].find(face => face.family === 'StylesheetOwner');
|
||||
const readyBefore = document.fonts.ready;
|
||||
const loaded = face.load();
|
||||
return JSON.stringify({
|
||||
globalThis.__stylesheetFontFaceProbe = {
|
||||
face: face instanceof FontFace,
|
||||
loaded: loaded === face.loaded,
|
||||
ready: readyBefore !== document.fonts.ready,
|
||||
status: document.fonts.status,
|
||||
get status() { return document.fonts.status; },
|
||||
observed
|
||||
});
|
||||
};
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("stylesheet FontFace loads should update document.fonts");
|
||||
)
|
||||
.expect("stylesheet FontFace loads should update document.fonts");
|
||||
|
||||
run_font_tasks(&mut vm);
|
||||
let result = vm
|
||||
.eval("JSON.stringify(globalThis.__stylesheetFontFaceProbe)")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
r#"{"face":true,"loaded":true,"ready":true,"status":"loaded","observed":["loading:0:true:true","loadingdone:1:true:true"]}"#
|
||||
|
||||
@@ -179,6 +179,8 @@ async fn child_document_fonts_ready_waits_for_its_own_resource() {
|
||||
.unwrap();
|
||||
vm.perform_owner_lane_task_microtask_checkpoints().unwrap();
|
||||
refresh_child_geometry_without_paint(&vm);
|
||||
assert_eq!(vm.eval("childReady").unwrap(), "false");
|
||||
run_font_tasks(&mut vm);
|
||||
assert_eq!(vm.eval("JSON.stringify([rootReady,childReady,child.document.fonts.status,childTarget.offsetWidth,childFace.status,childFaceLoaded])").unwrap(), r#"[true,true,"loaded",48,"loaded",true]"#);
|
||||
assert_eq!(vm.eval("String(parentTarget.offsetWidth)").unwrap(), parent);
|
||||
assert_eq!(vm.document_web_font_counts_for_test(), (0, 0, 0));
|
||||
|
||||
@@ -55,6 +55,8 @@ async fn document_fonts_ready_waits_for_shadow_font_and_refreshes_cached_geometr
|
||||
vm.perform_owner_lane_task_microtask_checkpoints()
|
||||
.expect("font completion checkpoint");
|
||||
assert_eq!(vm.document_web_font_counts_for_test(), (1, 1, 1));
|
||||
assert_eq!(vm.eval("fontReadyResolved").unwrap(), "false");
|
||||
run_font_tasks(&mut vm);
|
||||
assert_eq!(vm.eval("JSON.stringify([fontReadyResolved,document.fonts.status,fontProbe.getBoundingClientRect().width])").unwrap(),r#"[true,"loaded",48]"#);
|
||||
}
|
||||
|
||||
|
||||
@@ -565,6 +565,19 @@ fn refresh_layout_for_test(vm: &mut StandaloneScriptVmHarness) {
|
||||
);
|
||||
}
|
||||
|
||||
// These font domain tests supply network bodies directly. Run their queued
|
||||
// event tasks and checkpoints explicitly; Page dispatcher behavior is covered
|
||||
// separately by the font event ordering and Window retirement tests.
|
||||
fn run_font_tasks(vm: &mut ScriptVm) {
|
||||
for _ in 0..16 {
|
||||
if !vm.has_ready_timeout() {
|
||||
return;
|
||||
}
|
||||
assert!(vm.run_next_timeout_for_test().unwrap().consumed_heap_head());
|
||||
}
|
||||
panic!("font loading tasks did not become idle");
|
||||
}
|
||||
|
||||
fn publish_layout_for_test(vm: &mut StandaloneScriptVmHarness) {
|
||||
vm.publish_layout_for_test()
|
||||
.expect("fixture screenshot should succeed");
|
||||
|
||||
@@ -29,6 +29,8 @@ struct FixtureRuntimeState {
|
||||
dynamic_stylesheet_script_executed: Arc<FixtureEvent>,
|
||||
runtime_inline_module_executed: Arc<FixtureEvent>,
|
||||
alternate_stylesheet_probe_executed: Arc<FixtureEvent>,
|
||||
runtime_owned_async_script_executed: Arc<FixtureEvent>,
|
||||
dynamic_preparation_context_replaced: Arc<FixtureEvent>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -1425,7 +1427,7 @@ const DYNAMIC_TAXONOMY_ASYNC_FAST_JS: &str = "window.dynamicScriptTaxonomyOrder.
|
||||
const DYNAMIC_TAXONOMY_IN_ORDER_SLOW_JS: &str = "if (window.dynamicScriptTaxonomyOrder) { window.dynamicScriptTaxonomyOrder.push('in-order-slow'); window.dynamicScriptTaxonomyOrderResult = window.dynamicScriptTaxonomyOrder.join(','); } if (window.dynamicInOrderScriptOrder) { window.dynamicInOrderScriptOrder.push('in-order-slow'); window.dynamicInOrderScriptOrderResult = window.dynamicInOrderScriptOrder.join(','); }";
|
||||
const DYNAMIC_TAXONOMY_IN_ORDER_FAST_JS: &str = "window.dynamicInOrderScriptOrder.push('in-order-fast'); window.dynamicInOrderScriptOrderResult = window.dynamicInOrderScriptOrder.join(',');";
|
||||
const DYNAMIC_PREPARATION_CONTEXT_STALE_JS: &str = "window.staleDynamicRan = true; window.dynamicPreparationContextOrder = window.dynamicPreparationContextOrder || []; window.dynamicPreparationContextOrder.push('stale'); window.dynamicPreparationContextResult = window.dynamicPreparationContextOrder.join(',');";
|
||||
const DYNAMIC_PREPARATION_CONTEXT_OPEN_JS: &str = r#"document.open(); document.write(`<!doctype html><html><body><script>window.documentOpenAsyncRan = true; window.dynamicPreparationContextOrder = ['replacement']; window.dynamicPreparationContextResult = 'pending'; setTimeout(() => { window.dynamicPreparationContextResult = window.staleDynamicRan ? 'stale-ran' : 'replacement-only'; }, 0);</script></body></html>`); document.close();"#;
|
||||
const DYNAMIC_PREPARATION_CONTEXT_OPEN_JS: &str = r#"document.open(); document.write(`<!doctype html><html><body><script>window.documentOpenAsyncRan = true; fetch('/assets/dynamic_preparation_context_replaced'); window.dynamicPreparationContextOrder = ['replacement']; window.dynamicPreparationContextResult = 'pending'; setTimeout(() => { window.dynamicPreparationContextResult = window.staleDynamicRan ? 'stale-ran' : 'replacement-only'; }, 0);</script></body></html>`); document.close();"#;
|
||||
const DOCUMENT_WRITE_IMPLICIT_REPLACE_ASYNC_JS: &str = r#"window.documentWriteImplicitOrder.push('dynamic'); document.write(`<!doctype html><html><body><main id="new">new</main><script>window.documentWriteImplicitOrder.push('replacement'); window.documentWriteImplicitReplaceRan = true; window.documentWriteImplicitResult = 'pending'; window.documentWriteImplicitOrderResult = window.documentWriteImplicitOrder.join(','); setTimeout(() => { window.documentWriteImplicitResult = window.documentWriteImplicitStaleDeferRan ? 'stale-ran' : 'replacement-only'; window.documentWriteImplicitOrderResult = window.documentWriteImplicitOrder.join(','); }, 0);</script></body></html>`);"#;
|
||||
const DOCUMENT_WRITE_IMPLICIT_REPLACE_ASYNC_MODULE_JS: &str = r#"window.documentWriteImplicitModuleOrder.push('dynamic'); document.write(`<!doctype html><html><body><main id="new">new</main><script>window.documentWriteImplicitModuleOrder.push('replacement'); window.documentWriteImplicitModuleReplaceRan = true; window.documentWriteImplicitModuleResult = 'pending'; window.documentWriteImplicitModuleOrderResult = window.documentWriteImplicitModuleOrder.join(','); setTimeout(() => { window.documentWriteImplicitModuleResult = window.documentWriteImplicitStaleModuleRan ? 'stale-ran' : 'replacement-only'; window.documentWriteImplicitModuleOrderResult = window.documentWriteImplicitModuleOrder.join(','); }, 0);</script></body></html>`);"#;
|
||||
const DOCUMENT_WRITE_IMPLICIT_REPLACE_STALE_DEFER_JS: &str = "window.documentWriteImplicitOrder.push('stale-defer'); window.documentWriteImplicitStaleDeferRan = true; window.documentWriteImplicitOrderResult = window.documentWriteImplicitOrder.join(',');";
|
||||
|
||||
@@ -12,9 +12,6 @@ static PARSER_IMAGE_FETCH_POLICY_ASSET_REQUESTS: OnceLock<Mutex<HashMap<(String,
|
||||
static PARSE_TIME_ASYNC_CHUNKED_TAIL_GATES: OnceLock<
|
||||
Mutex<HashMap<String, Arc<tokio::sync::Notify>>>,
|
||||
> = OnceLock::new();
|
||||
static RUNTIME_OWNED_ASYNC_CHUNKED_TAIL_GATES: OnceLock<
|
||||
Mutex<HashMap<String, Arc<tokio::sync::Notify>>>,
|
||||
> = OnceLock::new();
|
||||
static RUNTIME_OWNED_IN_ORDER_ERROR_AFTER_DCL_GATES: OnceLock<
|
||||
Mutex<HashMap<String, Arc<tokio::sync::Notify>>>,
|
||||
> = OnceLock::new();
|
||||
@@ -98,35 +95,6 @@ fn notify_parse_time_async_chunked_tail_gate_if_present(host_key: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
fn runtime_owned_async_chunked_tail_gate(host_key: &str) -> Arc<tokio::sync::Notify> {
|
||||
let gates = RUNTIME_OWNED_ASYNC_CHUNKED_TAIL_GATES.get_or_init(|| Mutex::new(HashMap::new()));
|
||||
let mut gates = gates.lock();
|
||||
gates
|
||||
.entry(host_key.to_owned())
|
||||
.or_insert_with(|| Arc::new(tokio::sync::Notify::new()))
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn remove_runtime_owned_async_chunked_tail_gate(host_key: &str) {
|
||||
let Some(gates) = RUNTIME_OWNED_ASYNC_CHUNKED_TAIL_GATES.get() else {
|
||||
return;
|
||||
};
|
||||
gates.lock().remove(host_key);
|
||||
}
|
||||
|
||||
fn notify_runtime_owned_async_chunked_tail_gate_if_present(host_key: &str) {
|
||||
let Some(gates) = RUNTIME_OWNED_ASYNC_CHUNKED_TAIL_GATES.get() else {
|
||||
return;
|
||||
};
|
||||
let notify = {
|
||||
let gates = gates.lock();
|
||||
gates.get(host_key).cloned()
|
||||
};
|
||||
if let Some(notify) = notify {
|
||||
notify.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
fn runtime_owned_in_order_error_after_dcl_gate(host_key: &str) -> Arc<tokio::sync::Notify> {
|
||||
let gates =
|
||||
RUNTIME_OWNED_IN_ORDER_ERROR_AFTER_DCL_GATES.get_or_init(|| Mutex::new(HashMap::new()));
|
||||
@@ -2617,7 +2585,7 @@ pub(super) async fn runtime_owned_external_async_fast_does_not_overtake_domconte
|
||||
}
|
||||
|
||||
pub(super) async fn runtime_owned_external_async_fast_streaming_tail_page(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<FixtureRuntimeState>,
|
||||
) -> Response {
|
||||
let split_marker = "<main id=\"late\">";
|
||||
let split_index = RUNTIME_OWNED_EXTERNAL_ASYNC_FAST_DOES_NOT_OVERTAKE_DOMCONTENTLOADED_HTML
|
||||
@@ -2625,30 +2593,42 @@ pub(super) async fn runtime_owned_external_async_fast_streaming_tail_page(
|
||||
.expect("streaming runtime-owned async fixture must include late DOM marker");
|
||||
let (head, tail) = RUNTIME_OWNED_EXTERNAL_ASYNC_FAST_DOES_NOT_OVERTAKE_DOMCONTENTLOADED_HTML
|
||||
.split_at(split_index);
|
||||
let head = head.as_bytes().to_vec();
|
||||
let head = head
|
||||
.replace(
|
||||
"<head>",
|
||||
"<head><script>window.runtimeOwnedAsyncFastTailGated = true;</script>",
|
||||
)
|
||||
.into_bytes();
|
||||
let tail = tail.as_bytes().to_vec();
|
||||
let host_key = request_host_key(&headers).unwrap_or_default();
|
||||
let tail_gate = runtime_owned_async_chunked_tail_gate(&host_key);
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(4);
|
||||
tokio::spawn(async move {
|
||||
if tx
|
||||
.send(Ok::<Bytes, std::convert::Infallible>(Bytes::from(head)))
|
||||
.send(Ok::<Bytes, std::io::Error>(Bytes::from(head)))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
let _ = tokio::time::timeout(Duration::from_millis(500), tail_gate.notified()).await;
|
||||
// Keep the parser tail unavailable until the already-requested script
|
||||
// can complete. Chromium then permits this dynamic async task while
|
||||
// `document.readyState` is still `loading`.
|
||||
sleep(Duration::from_millis(20)).await;
|
||||
let _ = tx
|
||||
.send(Ok::<Bytes, std::convert::Infallible>(Bytes::from(tail)))
|
||||
.await;
|
||||
if !host_key.is_empty() {
|
||||
remove_runtime_owned_async_chunked_tail_gate(&host_key);
|
||||
// Wait for execution, not merely the script request arriving. A slow
|
||||
// renderer must not let the parser tail overtake the async task.
|
||||
if tokio::time::timeout(
|
||||
Duration::from_secs(5),
|
||||
state.runtime_owned_async_script_executed.wait(),
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
let _ = tx
|
||||
.send(Err(std::io::Error::new(
|
||||
std::io::ErrorKind::TimedOut,
|
||||
"async script did not release the parser tail",
|
||||
)))
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
let _ = tx
|
||||
.send(Ok::<Bytes, std::io::Error>(Bytes::from(tail)))
|
||||
.await;
|
||||
});
|
||||
|
||||
Response::builder()
|
||||
@@ -4429,13 +4409,17 @@ pub(super) async fn asset_runtime_owned_async_slow_script() -> Response {
|
||||
javascript_response(RUNTIME_OWNED_ASYNC_SLOW_JS)
|
||||
}
|
||||
|
||||
pub(super) async fn asset_runtime_owned_async_fast_script(headers: HeaderMap) -> Response {
|
||||
if let Some(host_key) = request_host_key(&headers) {
|
||||
notify_runtime_owned_async_chunked_tail_gate_if_present(&host_key);
|
||||
}
|
||||
pub(super) async fn asset_runtime_owned_async_fast_script() -> Response {
|
||||
javascript_response(RUNTIME_OWNED_ASYNC_FAST_JS)
|
||||
}
|
||||
|
||||
pub(super) async fn asset_runtime_owned_async_fast_executed(
|
||||
Extension(state): Extension<FixtureRuntimeState>,
|
||||
) -> StatusCode {
|
||||
state.runtime_owned_async_script_executed.signal();
|
||||
StatusCode::NO_CONTENT
|
||||
}
|
||||
|
||||
pub(super) async fn asset_runtime_owned_default_async_module_side_effect_module() -> Response {
|
||||
javascript_response(RUNTIME_OWNED_DEFAULT_ASYNC_MODULE_SIDE_EFFECT_MJS)
|
||||
}
|
||||
@@ -4642,11 +4626,32 @@ pub(super) async fn asset_document_write_inserted_chunked_external_script() -> R
|
||||
javascript_response(DOCUMENT_WRITE_INSERTED_CHUNKED_EXTERNAL_JS)
|
||||
}
|
||||
|
||||
pub(super) async fn asset_dynamic_preparation_context_stale_script() -> Response {
|
||||
sleep(Duration::from_millis(40)).await;
|
||||
pub(super) async fn asset_dynamic_preparation_context_stale_script(
|
||||
Extension(state): Extension<FixtureRuntimeState>,
|
||||
) -> Response {
|
||||
if tokio::time::timeout(
|
||||
Duration::from_secs(5),
|
||||
state.dynamic_preparation_context_replaced.wait(),
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return (
|
||||
StatusCode::GATEWAY_TIMEOUT,
|
||||
"replacement document did not release the stale script",
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
javascript_response(DYNAMIC_PREPARATION_CONTEXT_STALE_JS)
|
||||
}
|
||||
|
||||
pub(super) async fn asset_dynamic_preparation_context_replaced(
|
||||
Extension(state): Extension<FixtureRuntimeState>,
|
||||
) -> StatusCode {
|
||||
state.dynamic_preparation_context_replaced.signal();
|
||||
StatusCode::NO_CONTENT
|
||||
}
|
||||
|
||||
pub(super) async fn asset_dynamic_preparation_context_open_script() -> Response {
|
||||
javascript_response(DYNAMIC_PREPARATION_CONTEXT_OPEN_JS)
|
||||
}
|
||||
|
||||
@@ -2314,6 +2314,14 @@ pub(super) fn build_router() -> Router {
|
||||
"/assets/dynamic_preparation_context_open.js",
|
||||
get(asset_dynamic_preparation_context_open_script),
|
||||
)
|
||||
.route(
|
||||
"/assets/dynamic_preparation_context_replaced",
|
||||
get(asset_dynamic_preparation_context_replaced),
|
||||
)
|
||||
.route(
|
||||
"/assets/runtime_owned_async_fast_executed",
|
||||
get(asset_runtime_owned_async_fast_executed),
|
||||
)
|
||||
.route(
|
||||
"/assets/document_write_implicit_replace_async.js",
|
||||
get(asset_document_write_implicit_replace_async_script),
|
||||
|
||||
Reference in New Issue
Block a user