Files
vectorcraft/crates/engine/tests/removed_font.rs
T
dotCooCoo d1089270ba Fonts: a removed font that type used leaves the list at the next font scan
When type had used a font, removing the font while the app ran left
it in the font menus, and the type kept drawing in it until the app
restarted. The font list is the loaded faces plus the scanned catalog,
and a scan replaced the catalog but kept every loaded face.

A scan now drops each loaded face whose file it did not catalog, with
the face's cached outlines. A family with no face left leaves the font
lists, and type set in it lays out in the fallback font and is
reported missing, as when a document opens without the font. When the
font is installed again, the next scan lists it and the type draws in
it again. Bundled fonts and fonts added as bytes have no file and
stay.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-16 06:16:34 +07:00

77 lines
3.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! A font removed while the app runs: once the fonts are scanned again (`text.rescanFonts`), its
//! family is no longer listed, and type set in it, which had loaded it, is laid out in the fallback
//! font and listed as missing, until the font is installed again. One test in its own process, so
//! the process-wide font database and font folders start fresh, as when the app starts.
// Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code (AGENTS.md › Robustness).
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use serde_json::{Value, json};
use vectorcraft_doc::{NodeId, NodeKind};
use vectorcraft_engine::Session;
use vectorcraft_text::{FALLBACK_FAMILY, FontDb};
const FAMILY: &str = "Gonefont Sans";
/// `text.fonts`' status for [`FAMILY`] and the family that stands in for it.
fn status(s: &mut Session) -> (String, String) {
let listed = s.execute("text.fonts", &json!({})).unwrap();
let row = listed.as_array().unwrap().iter().find(|f| f["family"] == FAMILY).cloned().unwrap();
(row["status"].as_str().unwrap().into(), row["resolved"]["family"].as_str().unwrap().into())
}
/// Whether `text.fontList` lists [`FAMILY`].
fn listed(s: &mut Session) -> bool {
let r = s.execute("text.fontList", &json!({})).unwrap();
r["families"].as_array().unwrap().iter().any(|f| f == FAMILY)
}
/// The families of the faces the type object `id` is laid out in.
fn laid_out_in(s: &Session, id: u64) -> Vec<String> {
let st = s.doc().unwrap();
let Some(NodeKind::Text(t)) = st.doc.node(NodeId(id)).map(|n| &n.kind) else { panic!("no type object {id}") };
let db = FontDb::global();
let mut families: Vec<String> = vectorcraft_text::layout(db, t).glyphs.iter().map(|g| db.face_by_id(g.font_id).unwrap().family.clone()).collect();
families.dedup();
families
}
#[test]
fn type_in_a_removed_font_falls_back_after_a_rescan() {
let root = vectorcraft_testkit::temp_dir("removed-font");
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).unwrap();
vectorcraft_text::set_user_font_dirs(vec![root.clone()]);
let file = root.join("Gonefont.otf");
std::fs::write(&file, vectorcraft_testkit::fonts::renamed(FAMILY)).unwrap();
let mut s = Session::new();
s.execute("file.new", &json!({"width": 300, "height": 200})).unwrap();
let id = s.execute("text.create", &json!({"x": 10, "y": 40, "text": "Gone", "font": FAMILY})).unwrap()["id"].as_u64().unwrap();
assert_eq!(status(&mut s), ("exact".into(), FAMILY.into()));
assert!(listed(&mut s));
assert_eq!(laid_out_in(&s, id), [FAMILY]);
// Removed: after a rescan the type is laid out in the fallback font, without editing the document.
std::fs::remove_file(&file).unwrap();
s.doc_mut().unwrap().mark_saved();
let revision = s.doc().unwrap().revision;
let r: Value = s.execute("text.rescanFonts", &json!({})).unwrap();
assert!(r["faces"].is_u64(), "{r}");
assert_eq!(status(&mut s), ("missing".into(), FALLBACK_FAMILY.into()));
assert!(!listed(&mut s));
assert!(s.execute("text.fontList", &json!({"family": FAMILY})).is_err());
assert_eq!(laid_out_in(&s, id), [FALLBACK_FAMILY]);
let missing = s.execute("text.missingFonts", &json!({})).unwrap();
assert_eq!(missing["fonts"][0]["family"], FAMILY, "{missing}");
let st = s.doc().unwrap();
assert!(st.revision > revision && !st.is_dirty());
// Installed again: the type is set in it again.
std::fs::write(&file, vectorcraft_testkit::fonts::renamed(FAMILY)).unwrap();
s.execute("text.rescanFonts", &json!({})).unwrap();
assert_eq!(status(&mut s), ("exact".into(), FAMILY.into()));
assert!(listed(&mut s));
assert_eq!(laid_out_in(&s, id), [FAMILY]);
let _ = std::fs::remove_dir_all(&root);
}