From bd320e65a51de2fe96f8dec1952f3832a6f9ab28 Mon Sep 17 00:00:00 2001 From: vangel76 Date: Sat, 17 Oct 2026 21:14:47 +0200 Subject: [PATCH] M2.1: pin layout tests to bundled fonts (#2729) --- ROADMAP.md | 1 + crates/fonts/Cargo.toml | 4 + crates/fonts/src/fontdb.rs | 32 +++- crates/fonts/tests/fixed_test_fonts.rs | 31 ++++ crates/fonts/tests/system_fonts.rs | 8 + crates/layout/Cargo.toml | 3 + crates/layout/src/contour.rs | 4 + crates/layout/src/fields.rs | 2 + crates/layout/src/kinsoku.rs | 1 + crates/layout/src/lib.rs | 7 + crates/layout/src/math.rs | 14 ++ crates/layout/src/outline.rs | 1 + crates/layout/src/para.rs | 6 + crates/layout/src/styleref.rs | 2 + crates/layout/src/tests.rs | 246 ++++++++++++++++++++++++- crates/layout/src/tests_bidi.rs | 21 +++ crates/layout/src/tests_complex.rs | 4 + crates/layout/src/tests_grid.rs | 2 + crates/layout/src/tests_numeral.rs | 8 + crates/layout/src/tests_pages.rs | 14 +- crates/layout/src/tests_styleref.rs | 9 + crates/layout/src/tests_textbox.rs | 4 + crates/layout/src/tests_vertical.rs | 4 + crates/layout/src/wordart.rs | 2 + docs/testing.md | 28 +++ 25 files changed, 448 insertions(+), 10 deletions(-) create mode 100644 crates/fonts/tests/fixed_test_fonts.rs create mode 100644 docs/testing.md diff --git a/ROADMAP.md b/ROADMAP.md index 40407d18b6..6700db9e2c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -135,6 +135,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm ## Progress log | Date | What landed | +| 2026-10-17 | Layout unit tests use the pinned bundled fonts without system/user fonts or optional craft-fonts inputs (#2729); every test initializes the fixture before font lookup, and pagination fixtures are measured against that fixed set | | 2026-10-14 | DOCX row table border exceptions render and survive saving. Explicit cell borders win; a row exception does not change other rows. | | 2026-10-14 | Tab stops edited on the ruler: the corner selector picks Left/Center/Right/Decimal/Bar, a click sets a stop (Alt skips the snap), dragging moves it with a guide line and position tooltip, dragging off the ruler clears it; markers draw per kind and mirror for RTL; one `para.tabs` call and one undo step per gesture | | 2026-10-12 | Fields keep `w:dirty` through DOCX saves (#877): a begin marker flagged by a generator that couldn't compute a result (pandoc-style TOC placeholders) keeps the flag on open/save — as a buffered field or a range field — so Word still refreshes it on open; computing a result (Update Field, Update Table) clears it, like Word's F9 | diff --git a/crates/fonts/Cargo.toml b/crates/fonts/Cargo.toml index 9107da44f1..d9d7812b11 100644 --- a/crates/fonts/Cargo.toml +++ b/crates/fonts/Cargo.toml @@ -8,6 +8,10 @@ description = "WordCraft fonts: font database (bundled OFL + system), metrics, g [lints] workspace = true +[features] +# Opt-in test harness support; enabling this alone never changes the application database. +test-support = [] + [dependencies] kurbo = { workspace = true } skrifa = { workspace = true } diff --git a/crates/fonts/src/fontdb.rs b/crates/fonts/src/fontdb.rs index 5f07295138..6b02089169 100644 --- a/crates/fonts/src/fontdb.rs +++ b/crates/fonts/src/fontdb.rs @@ -40,6 +40,8 @@ static BUNDLED: &[&[u8]] = &[ include_bytes!("../../../assets/fonts/JetBrainsMono-Regular.ttf"), ]; +static GLOBAL: std::sync::OnceLock = std::sync::OnceLock::new(); + #[derive(Clone)] enum FontBytes { Static(&'static [u8]), @@ -894,10 +896,14 @@ impl FontDb { /// Arabic, so Arabic and Persian text draws without system fonts (#1652) (all empty without /// `CRAFT_FONTS_DIR`), whose system font scan reads `font_dirs`. pub fn with_font_dirs(font_dirs: Vec) -> Self { - let mut faces = Vec::new(); let others = crate::CRAFT_FONTS.iter().filter(|f| !f.scripts.contains(&"Jpan")); let craft = crate::japanese_document_fonts().into_iter().chain(others).map(|f| f.bytes); - for data in BUNDLED.iter().copied().chain(craft) { + Self::with_static_fonts(font_dirs, BUNDLED.iter().copied().chain(craft)) + } + + fn with_static_fonts(font_dirs: Vec, fonts: impl Iterator) -> Self { + let mut faces = Vec::new(); + for data in fonts { for f in enumerate_faces(data) { if let Some(mut face) = make_face(FontBytes::Static(data), f.index, f.family, f.style, f.legacy, f.coords) { face.local_names = f.local; @@ -948,8 +954,26 @@ impl FontDb { /// first time a lookup by family name needs them (or ahead of time by /// [`FontDb::scan_in_background`]). pub fn global() -> &'static FontDb { - static DB: std::sync::OnceLock = std::sync::OnceLock::new(); - DB.get_or_init(|| FontDb::with_font_dirs(system_font_dirs())) + GLOBAL.get_or_init(|| FontDb::with_font_dirs(system_font_dirs())) + } + + /// Initialize a test process with only the repository's pinned bundled fonts: no optional + /// craft-fonts, user fonts or system font directories. Call before any global font lookup. + /// Returns false if a normal global database was already initialized; never replaces it. + /// Repeated/concurrent calls are safe. Merely enabling `test-support` changes nothing. + #[cfg(feature = "test-support")] + pub fn init_test_fonts() -> bool { + static INIT: std::sync::OnceLock = std::sync::OnceLock::new(); + *INIT.get_or_init(|| { + let mut installed = false; + GLOBAL.get_or_init(|| { + installed = true; + let db = Self::with_static_fonts(Vec::new(), BUNDLED.iter().copied()); + db.set_system_fallback(false); + db + }); + installed + }) } fn read_faces(&self) -> std::sync::RwLockReadGuard<'_, Vec>> { diff --git a/crates/fonts/tests/fixed_test_fonts.rs b/crates/fonts/tests/fixed_test_fonts.rs new file mode 100644 index 0000000000..3f644ff624 --- /dev/null +++ b/crates/fonts/tests/fixed_test_fonts.rs @@ -0,0 +1,31 @@ +//! Separate process: verifies the global test fixture before any other lookup initializes it. +#![cfg(feature = "test-support")] +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] + +use wordcraft_fonts::FontDb; + +#[test] +fn fixed_fonts_exclude_system_and_optional_faces() { + let threads: Vec<_> = (0..8).map(|_| std::thread::spawn(FontDb::init_test_fonts)).collect(); + assert!(threads.into_iter().all(|t| t.join().unwrap())); + let db = FontDb::global(); + let families = [ + "Inter", + "Inter Medium", + "Inter SemiBold", + "JetBrains Mono", + "Source Sans 3", + "Source Sans 3 Semibold", + "Source Serif 4", + "Source Serif 4 Semibold", + ]; + assert_eq!(db.families(), families); + assert_eq!(db.load_system_fonts(), 0); + assert_eq!(db.families(), families); + for family in ["Arial", "Calibri", "Carlito", "Noto Sans Arabic", "BIZ UDPGothic"] { + assert!(!db.has_family(family), "external family {family} leaked into the test fixture"); + } + assert!(db.fallback_for('你', 0).is_none(), "missing glyph lookup must not consult system fonts"); + assert_eq!(db.loaded_font_bytes(), 0); + assert!(FontDb::init_test_fonts()); +} diff --git a/crates/fonts/tests/system_fonts.rs b/crates/fonts/tests/system_fonts.rs index e31fb1fe78..d686394abf 100644 --- a/crates/fonts/tests/system_fonts.rs +++ b/crates/fonts/tests/system_fonts.rs @@ -21,3 +21,11 @@ fn a_fresh_session_finds_installed_fonts_by_name() { assert_eq!(&FontDb::global().face(family, "Regular").family, family); assert!(FontDb::global().families().contains(family)); } + +#[cfg(feature = "test-support")] +#[test] +fn test_initialization_cannot_replace_an_application_database() { + let db = FontDb::global(); + assert!(!FontDb::init_test_fonts()); + assert!(std::ptr::eq(db, FontDb::global())); +} diff --git a/crates/layout/Cargo.toml b/crates/layout/Cargo.toml index afb8f0a4a1..0a5cf79fe6 100644 --- a/crates/layout/Cargo.toml +++ b/crates/layout/Cargo.toml @@ -25,3 +25,6 @@ log = { workspace = true } [target.'cfg(target_arch = "wasm32")'.dependencies] # The browser clock (`SystemTime::now()`/`Instant::now()` panic on wasm32). js-sys = "0.3" + +[dev-dependencies] +wordcraft-fonts = { workspace = true, features = ["test-support"] } diff --git a/crates/layout/src/contour.rs b/crates/layout/src/contour.rs index 1c245b885d..e2e5fce610 100644 --- a/crates/layout/src/contour.rs +++ b/crates/layout/src/contour.rs @@ -221,6 +221,7 @@ mod tests { #[test] fn occupied_follows_the_outline() { + crate::test_fonts(); let c = diamond(false); let s = c.occupied(0.0, 10.0); assert_eq!(s.len(), 1); @@ -235,6 +236,7 @@ mod tests { #[test] fn through_keeps_interior_gaps() { + crate::test_fonts(); // A U: two prongs joined at the bottom. let u = [(0.0, 0.0), (20.0, 0.0), (20.0, 80.0), (80.0, 80.0), (80.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)]; let (_, tight) = Contour::new(&u, 0.0, 0.0, 0.0, false).unwrap(); @@ -246,6 +248,7 @@ mod tests { #[test] fn pictures_wrap_around_their_opaque_pixels() { + crate::test_fonts(); let png = |img: image::DynamicImage| { let mut buf = Vec::new(); img.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png).unwrap(); @@ -268,6 +271,7 @@ mod tests { #[test] fn hostile_points_are_dropped() { + crate::test_fonts(); assert!(Contour::new(&[(f32::NAN, 0.0), (1.0, 1.0), (f32::INFINITY, 2.0)], 0.0, 0.0, 0.0, false).is_none()); let c = diamond(true); assert!(c.occupied(f32::NAN, 1.0).is_empty()); diff --git a/crates/layout/src/fields.rs b/crates/layout/src/fields.rs index e0300a06d1..f0f4a61ac3 100644 --- a/crates/layout/src/fields.rs +++ b/crates/layout/src/fields.rs @@ -177,6 +177,7 @@ mod tests { #[test] fn page_fields() { + crate::test_fonts(); let ctx = FieldCtx { page: 4, pages: 12, ..Default::default() }; assert_eq!(field_text("PAGE", "", &ctx), ("4".into(), true)); assert_eq!(field_text(" PAGE \\* ROMAN ", "", &ctx).0, "IV"); @@ -191,6 +192,7 @@ mod tests { #[test] fn page_references_show_their_bookmarks_page() { + crate::test_fonts(); let pages = HashMap::from([("End_1".to_string(), (3, NumFormat::Decimal)), ("Intro".to_string(), (4, NumFormat::LowerRoman))]); let ctx = FieldCtx { bookmark_pages: Arc::new(pages), bookmarks_key: 1, ..Default::default() }; assert_eq!(field_text("PAGEREF End_1 \\* Arabic \\* MERGEFORMAT", "9", &ctx), ("3".into(), true)); diff --git a/crates/layout/src/kinsoku.rs b/crates/layout/src/kinsoku.rs index 38f8df4362..b0d6b2cbea 100644 --- a/crates/layout/src/kinsoku.rs +++ b/crates/layout/src/kinsoku.rs @@ -82,6 +82,7 @@ mod tests { #[test] fn kinsoku_and_word_wrap_break_opportunities() { + crate::test_fonts(); let ja = "日本語、「漢字」。テスト"; let on = breaks(ja, true, true); for c in ["、", "」", "。", "漢"] { diff --git a/crates/layout/src/lib.rs b/crates/layout/src/lib.rs index 3ff6484361..947d8b0e42 100644 --- a/crates/layout/src/lib.rs +++ b/crates/layout/src/lib.rs @@ -8,6 +8,13 @@ //! - [`hit`]: point ↔ position, caret geometry, line navigation, selection rectangles. #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] +/// Every unit test calls this before any font lookup (including its fixture construction). +/// A process-wide immutable choice keeps font and math caches consistent across test threads. +#[cfg(test)] +fn test_fonts() { + assert!(wordcraft_fonts::FontDb::init_test_fonts(), "layout tests must initialize fixed fonts before any global font lookup"); +} + pub mod balloons; mod contour; pub mod display; diff --git a/crates/layout/src/math.rs b/crates/layout/src/math.rs index bab58ef984..7b558a2a44 100644 --- a/crates/layout/src/math.rs +++ b/crates/layout/src/math.rs @@ -1996,6 +1996,7 @@ mod tests { #[test] fn long_display_equations_wrap_before_operators_and_indent() { + crate::test_fonts(); let sum = "y=a+b+c+d+e+f+g+h+i+j+k+l+m+n+o+p+q+r+s+t+u+v+w"; let mp = MathProps::default(); let full = lay_in(&Math::default(), sum, true, 1e4, &mp); @@ -2036,6 +2037,7 @@ mod tests { #[test] fn manual_breaks_start_new_lines() { + crate::test_fonts(); let mp = MathProps::default(); let nodes = vec![MNode::Run(MRun::new("a=b")), MNode::Run(MRun { brk: Some(0), ..MRun::new("+c") })]; let m = lay_in(&Math { nodes, ..Default::default() }, "", true, 400.0, &mp); @@ -2052,6 +2054,7 @@ mod tests { #[test] fn short_display_equations_stay_on_one_line() { + crate::test_fonts(); let mp = MathProps::default(); let one = lay_in(&Math::default(), "a+b=c", true, 400.0, &mp); assert_eq!(lines(&one).len(), 1); @@ -2061,6 +2064,7 @@ mod tests { #[test] fn fraction_is_stacked_with_a_rule() { + crate::test_fonts(); let flat = lay("ab", true); let f = lay("a/b", true); assert!(f.ascent > flat.ascent + 2.0 && f.descent > flat.descent + 2.0, "{f:?}"); @@ -2070,6 +2074,7 @@ mod tests { #[test] fn superscript_is_raised_and_smaller() { + crate::test_fonts(); let m = lay("x^2", false); let sizes: Vec<(f32, f32)> = m .items @@ -2085,6 +2090,7 @@ mod tests { #[test] fn operators_get_spaces_and_unary_minus_does_not() { + crate::test_fonts(); let tight = lay("ab", false); let spaced = lay("a+b", false); let unary = lay("−b", false); @@ -2095,6 +2101,7 @@ mod tests { #[test] fn display_operators_are_big_with_limits_over_and_under() { + crate::test_fonts(); let d = lay("∑_(i=1)^n▒i", true); let t = lay("∑_(i=1)^n▒i", false); assert!(d.ascent + d.descent > t.ascent + t.descent, "display sum taller"); @@ -2103,6 +2110,7 @@ mod tests { #[test] fn delimiters_grow_around_tall_content() { + crate::test_fonts(); let small = lay("(x)", true); let big = lay("((a)/(b))", true); let h = |m: &MathLayout| m.ascent + m.descent; @@ -2111,6 +2119,7 @@ mod tests { #[test] fn every_structure_lays_out() { + crate::test_fonts(); use wordcraft_doc::math::*; let x = || vec![MNode::Run(MRun::new("x"))]; let nodes = vec![ @@ -2146,6 +2155,7 @@ mod tests { #[test] fn caret_slots_cover_every_position() { + crate::test_fonts(); use wordcraft_doc::math_edit::{MathPos, move_right, units}; let math = Math { nodes: wordcraft_doc::math::parse_linear("x=(-b±√(b^2-4ac))/2a+■(a&b@c&d)"), ..Default::default() }; let m = layout_equation(&math, "", &rc(), true, 400.0, &MathProps::default(), &mut 0); @@ -2171,6 +2181,7 @@ mod tests { #[test] fn empty_slots_show_placeholders_on_screen_only() { + crate::test_fonts(); let math = Math { nodes: wordcraft_doc::math::parse_linear("⬚/⬚"), ..Default::default() }; let m = layout_equation(&math, "", &rc(), true, 400.0, &MathProps::default(), &mut 0); assert!(m.items.iter().all(|i| matches!(i, MItem::ScreenOnly(_) | MItem::Rect { .. })), "{:?}", m.items); @@ -2181,6 +2192,7 @@ mod tests { #[test] fn numbered_equations_fill_the_line_with_the_number_at_the_right() { + crate::test_fonts(); let math = Math { nodes: wordcraft_doc::math::parse_linear("E=mc^2#(1)"), ..Default::default() }; let m = layout_equation(&math, "", &rc(), true, 400.0, &MathProps::default(), &mut 0); assert!((m.width - 400.0).abs() < 0.01, "{}", m.width); @@ -2200,6 +2212,7 @@ mod tests { #[test] fn math_alphabets() { + crate::test_fonts(); assert_eq!(math_alnum('x', false, true, MScr::Roman), '𝑥'); assert_eq!(math_alnum('h', false, true, MScr::Roman), 'ℎ'); assert_eq!(math_alnum('R', false, false, MScr::DoubleStruck), 'ℝ'); @@ -2210,6 +2223,7 @@ mod tests { #[test] fn hostile_trees_never_panic() { + crate::test_fonts(); use wordcraft_doc::math::*; // Deep nesting. let mut n = vec![MNode::Run(MRun::new("x"))]; diff --git a/crates/layout/src/outline.rs b/crates/layout/src/outline.rs index af72e838dc..27b86657c5 100644 --- a/crates/layout/src/outline.rs +++ b/crates/layout/src/outline.rs @@ -222,6 +222,7 @@ mod tests { #[test] fn show_level_and_collapse_hide_what_is_under_a_heading() { + crate::test_fonts(); // H1, body, H2, body, H1, body let levels = [1, BODY, 2, BODY, 1, BODY]; let all = OutlineView::default(); diff --git a/crates/layout/src/para.rs b/crates/layout/src/para.rs index 6b1ea46fd4..7c517e213a 100644 --- a/crates/layout/src/para.rs +++ b/crates/layout/src/para.rs @@ -2743,6 +2743,7 @@ mod label_spacing_tests { #[test] fn large_label_overflow_is_not_multiplied() { + crate::test_fonts(); for (multiple, expected) in [(1.0, 25.20), (1.25, 28.80), (2.0, 39.36)] { let lines = measured_label_lines(LineSpacing::Multiple(multiple), (22.265625, 5.859375), 22.265625, false); assert!((lines[0].height - expected).abs() <= 0.25, "{multiple}: {} vs {expected}", lines[0].height); @@ -2754,12 +2755,14 @@ mod label_spacing_tests { #[test] fn small_label_descent_does_not_inflate_text_spacing() { + crate::test_fonts(); let lines = measured_label_lines(LineSpacing::Multiple(1.25), (8.499023, 3.5009766), 9.0, false); assert!((lines[0].height - 17.52).abs() <= 0.25, "{}", lines[0].height); } #[test] fn label_leading_and_earlier_font_ascent_are_retained() { + crate::test_fonts(); // Apple Symbols 24 pt: full leading gives an 18 pt label ascent. The text // pitch is still Roboto 12 pt. A shorter final fallback run must not hide it. for metrics in [(16.998047, 7.001953), (8.0, 3.0)] { @@ -2770,6 +2773,7 @@ mod label_spacing_tests { #[test] fn minimum_spacing_keeps_text_descent_and_label_leading() { + crate::test_fonts(); for (metrics, ascent, expected) in [((22.265625, 5.859375), 22.265625, 25.20), ((8.499023, 3.5009766), 9.0, 13.92), ((16.998047, 7.001953), 18.0, 20.88)] { @@ -2780,6 +2784,7 @@ mod label_spacing_tests { #[test] fn labels_keep_fixed_and_minimum_spacing_rules() { + crate::test_fonts(); for (spacing, expected) in [(LineSpacing::Exactly(24.0), 24.0), (LineSpacing::AtLeast(30.0), 30.0)] { let lines = measured_label_lines(spacing, (22.265625, 5.859375), 22.265625, false); assert!((lines[0].height - expected).abs() < 0.001); @@ -2788,6 +2793,7 @@ mod label_spacing_tests { #[test] fn label_overflow_only_affects_the_first_wrapped_line() { + crate::test_fonts(); let lines = measured_label_lines(LineSpacing::Multiple(1.25), (22.265625, 5.859375), 22.265625, true); assert!(lines.len() > 1); assert!((lines[0].height - 28.80).abs() <= 0.25); diff --git a/crates/layout/src/styleref.rs b/crates/layout/src/styleref.rs index 02a528441f..1d336903fa 100644 --- a/crates/layout/src/styleref.rs +++ b/crates/layout/src/styleref.rs @@ -347,6 +347,7 @@ mod tests { #[test] fn parses_names_levels_and_switches() { + crate::test_fonts(); let c = parse(r#"STYLEREF "Heading 1" \l \n \t \* MERGEFORMAT"#); assert_eq!(c.style, "Heading 1"); assert!(c.last && c.suppress && !c.position); @@ -359,6 +360,7 @@ mod tests { #[test] fn suppress_keeps_the_number() { + crate::test_fonts(); assert_eq!(suppress("Chapter 2.", &[("2".into(), true)]), "2"); assert_eq!(chapter_number("Chapter IV."), "IV"); } diff --git a/crates/layout/src/tests.rs b/crates/layout/src/tests.rs index d6865b0d92..bfe8761507 100644 --- a/crates/layout/src/tests.rs +++ b/crates/layout/src/tests.rs @@ -61,6 +61,7 @@ fn legacy_table_gap(mode: u32, allow: bool, location: SpacingLocation) -> f32 { #[test] fn legacy_table_contextual_spacing_flag_keeps_after_space() { + crate::test_fonts(); // Original Word for Mac 16.113.3 probe: after 12 pt, before 0 pt, both contextual. // Zero before-space keeps the compatibility rule independent of ordinary overlap. for mode in [11, 12, 14] { @@ -71,6 +72,7 @@ fn legacy_table_contextual_spacing_flag_keeps_after_space() { #[test] fn legacy_table_contextual_spacing_flag_is_scoped_to_cells_and_legacy_mode() { + crate::test_fonts(); for (mode, allow) in [(11, false), (12, false), (14, false), (15, false), (15, true)] { assert!(legacy_table_gap(mode, allow, SpacingLocation::Cell).abs() < 0.01); } @@ -123,6 +125,7 @@ fn contextual_gap(after: f32, before: f32, first: bool, second: bool, different_ // Exactly 24 pt spacing isolates the extra gap from font-dependent line height. #[test] fn contextual_after_removes_a_smaller_before_gap() { + crate::test_fonts(); for header in [false, true] { let gap = contextual_gap(12.0, 8.0, true, false, false, header); assert!(gap.abs() < 0.01, "header={header}: gap={gap}, expected 0 pt"); @@ -131,6 +134,7 @@ fn contextual_after_removes_a_smaller_before_gap() { #[test] fn contextual_after_keeps_only_the_residual_before_gap() { + crate::test_fonts(); // ISO/IEC 29500-1 §17.3.1.9's worked example: max(10, 12) - 10 = 2 pt. for header in [false, true] { let gap = contextual_gap(10.0, 12.0, true, false, false, header); @@ -140,6 +144,7 @@ fn contextual_after_keeps_only_the_residual_before_gap() { #[test] fn contextual_spacing_preserves_each_side_and_style_identity() { + crate::test_fonts(); for header in [false, true] { for (after, before, first, second, different, expected) in [ (12.0, 8.0, false, true, false, 12.0), @@ -158,6 +163,7 @@ fn contextual_spacing_preserves_each_side_and_style_identity() { #[test] fn contextual_spacing_removes_every_gap_in_a_four_paragraph_run() { + crate::test_fonts(); use wordcraft_doc::props::LineSpacing; for table in [false, true] { let mut doc = Document::new(); @@ -207,6 +213,7 @@ fn contextual_spacing_removes_every_gap_in_a_four_paragraph_run() { // Contextual and automatic list spacing can suppress the same after-space only once. #[test] fn contextual_automatic_lists_do_not_subtract_the_gap_twice() { + crate::test_fonts(); use wordcraft_doc::props::{LineSpacing, NumRef}; for table in [false, true] { let mut doc = Document::new(); @@ -255,6 +262,7 @@ fn lines_of(l: &DocLayout) -> usize { #[test] fn empty_doc_has_one_page() { + crate::test_fonts(); let d = Document::new(); let l = lay(&d); assert_eq!(l.pages.len(), 1); @@ -267,6 +275,7 @@ fn empty_doc_has_one_page() { #[test] fn hidden_marks_take_no_space() { + crate::test_fonts(); let mut d = Document::new(); d.body = vec![wordcraft_doc::para_block(wordcraft_doc::Paragraph::with_text("Before", Default::default()))]; for _ in 0..20 { @@ -308,6 +317,7 @@ fn hidden_marks_take_no_space() { #[test] fn long_paragraph_wraps_and_paginates() { + crate::test_fonts(); let text = "The quick brown fox jumps over the lazy dog. ".repeat(400); let d = Document::from_text(&text); let l = lay(&d); @@ -328,6 +338,7 @@ fn long_paragraph_wraps_and_paginates() { #[test] fn reading_layout_reflows_into_viewport_height_pages_without_changing_print_layout() { + crate::test_fonts(); let d = Document::from_text(&"A moderately long sentence that should wrap across reading columns. ".repeat(250)); let opts = LayoutOptions { view: ViewMode::Read, web_width: 230.0, read_height: 390.0, ..Default::default() }; let read = layout(&d, &mut LayoutCache::new(), &opts); @@ -352,6 +363,7 @@ fn reading_layout_reflows_into_viewport_height_pages_without_changing_print_layo #[test] fn hit_and_caret_agree() { + crate::test_fonts(); let d = Document::from_text("Hello world, this is WordCraft.\nSecond paragraph here."); let l = lay(&d); for off in [0, 5, 12, 31] { @@ -370,6 +382,7 @@ fn hit_and_caret_agree() { #[test] fn selection_bar_finds_the_line_beside_the_pointer() { + crate::test_fonts(); // #856: the left margin beside a line is Word's selection bar. let text = format!("{}\nShort.", "The quick brown fox jumps over the lazy dog. ".repeat(8)); let mut d = Document::from_text(&text); @@ -403,6 +416,7 @@ fn selection_bar_finds_the_line_beside_the_pointer() { #[test] fn deletions_leave_the_final_text_layout() { + crate::test_fonts(); // "Keep " + deleted "DELETEDTEXT " (5..17) + inserted "INSERTED". let mut d = Document::from_text("Keep DELETEDTEXT INSERTED"); d.format_range(&Pos::body(0, 5), &Pos::body(0, 17), &|c| c.del = Some(0)).unwrap(); @@ -433,6 +447,7 @@ fn deletions_leave_the_final_text_layout() { #[test] fn deleted_soft_hyphen_is_no_hyphenation_point_without_markup() { + crate::test_fonts(); // "ab\u{ad}cdef" in a column where "ab-" fits but the whole word does not; only the soft // hyphen is track-deleted. let mut d = Document::from_text("ab\u{ad}cdef"); @@ -451,6 +466,7 @@ fn deleted_soft_hyphen_is_no_hyphenation_point_without_markup() { #[test] fn deleted_text_adds_no_line_break_opportunity_without_markup() { + crate::test_fonts(); // "xx ab\u{ad}cdef" in a column where "xx ab-" and "abcdef" fit but "xx abcdef" does not; only // the soft hyphen (5..7) is track-deleted. let mut d = Document::from_text("xx ab\u{ad}cdef"); @@ -469,6 +485,7 @@ fn deleted_text_adds_no_line_break_opportunity_without_markup() { #[test] fn hidden_soft_hyphen_is_no_hyphenation_point_when_hidden_text_is_not_shown() { + crate::test_fonts(); // As with deletions: "ab\u{ad}cdef" where only the soft hyphen is hidden text. let mut d = Document::from_text("ab\u{ad}cdef"); d.format_range(&Pos::body(0, 2), &Pos::body(0, 4), &|c| c.hidden = Some(true)).unwrap(); @@ -486,6 +503,7 @@ fn hidden_soft_hyphen_is_no_hyphenation_point_when_hidden_text_is_not_shown() { #[test] fn hidden_text_adds_no_line_break_opportunity_when_not_shown() { + crate::test_fonts(); // "xx ab" + hidden soft hyphen or space + "cdef", in a column where "xx ab-" and "abcdef" fit // but "xx abcdef" does not. Measured in Word (hidden text not printed): "xx" / "abcdef" both times. for hidden in ["\u{ad}", " "] { @@ -558,6 +576,7 @@ fn layout_time_ratio(doc: impl Fn(usize) -> Document, n: usize, opts: &LayoutOpt #[test] fn hidden_separators_with_automatic_hyphenation_lay_out_in_linear_time() { + crate::test_fonts(); // "hyphenation " n times with every space hidden: one joined word of 11n letters reaches the // hyphenator, whose limit checks once rescanned the word for every candidate point. let doc = |n: usize| { @@ -576,6 +595,7 @@ fn hidden_separators_with_automatic_hyphenation_lay_out_in_linear_time() { #[test] fn many_hidden_float_anchors_lay_out_in_linear_time() { + crate::test_fonts(); // n square-wrapped shapes, each anchored in its own hidden run (bold alternating, so runs do not // merge), markup on: finding each anchor's formatting once rescanned the runs from the start. let doc = |n: usize| { @@ -630,6 +650,7 @@ fn many_hidden_float_anchors_lay_out_in_linear_time() { #[test] fn fragmented_hidden_text_lays_out_like_the_visible_text() { + crate::test_fonts(); // Visible words with a hidden multi-byte char after every visible one, formatted in alternating // runs (bold on and off) so no two left-out runs are adjacent or merge. The plain paragraph has // the same visible runs without the hidden chars, so both lay out the same widths whatever font @@ -684,6 +705,7 @@ fn fragmented_hidden_text_lays_out_like_the_visible_text() { #[test] fn automatic_hyphenation_sees_only_the_text_laid_out() { + crate::test_fonts(); // Paragraph-text ends of the clusters after which a line may end with a hyphen. let hyph_ends = |d: &Document, opts: &LayoutOptions| { let l = layout(d, &mut LayoutCache::new(), opts); @@ -723,6 +745,7 @@ fn automatic_hyphenation_sees_only_the_text_laid_out() { #[test] fn page_break_char_starts_new_page() { + crate::test_fonts(); let mut d = Document::from_text("one"); d.insert_text(&Pos::body(0, 3), "\u{000C}", &Default::default()).unwrap(); let l = lay(&d); @@ -731,6 +754,7 @@ fn page_break_char_starts_new_page() { #[test] fn a_missing_roman_font_is_drawn_as_word_draws_it() { + crate::test_fonts(); // Measured in Word: a font that isn't installed is drawn in Cambria when the font table calls // it roman, in Calibri otherwise. let face = |roman: bool| { @@ -752,6 +776,7 @@ fn a_missing_roman_font_is_drawn_as_word_draws_it() { #[test] fn an_empty_section_mark_stays_on_a_full_page() { + crate::test_fonts(); use wordcraft_doc::section::SectionProps; // Measured in Word: a page filled to the bottom, then an empty paragraph carrying a next-page // section break, makes no page for the mark; the next section starts on page 2. A plain empty @@ -775,6 +800,7 @@ fn an_empty_section_mark_stays_on_a_full_page() { #[test] fn a_bookmark_after_a_page_break_takes_no_line() { + crate::test_fonts(); // "Before", a page break, then a bookmark start (Word writes the next heading's TOC bookmark // there): the bookmark takes no room, so "After" starts at the top of page 2, as in Word. let top_of_after = |bookmark: bool| { @@ -797,6 +823,7 @@ fn a_bookmark_after_a_page_break_takes_no_line() { #[test] fn a_page_break_then_a_new_page_makes_one_page() { + crate::test_fonts(); use wordcraft_doc::section::SectionProps; // "Before", a paragraph of only a page break, then "After" starting a page by itself: a page // break before, or a next-page section break carried by the break's own paragraph. Word's @@ -819,6 +846,7 @@ fn a_page_break_then_a_new_page_makes_one_page() { #[test] fn page_numbers_can_restart_at_zero() { + crate::test_fonts(); // `w:pgNumType w:start="0"`: a cover page numbered 0, so the page after it reads 1. let mut d = Document::from_text("Cover\u{000C}Contents"); d.last_section.page_num_start = Some(0); @@ -828,6 +856,7 @@ fn page_numbers_can_restart_at_zero() { #[test] fn center_and_right_alignment() { + crate::test_fonts(); let mut d = Document::from_text("abc\nabc\nabc"); d.format_paragraphs(&Pos::body(1, 0), &Pos::body(1, 0), &|p| p.align = Some(Align::Center)).unwrap(); d.format_paragraphs(&Pos::body(2, 0), &Pos::body(2, 0), &|p| p.align = Some(Align::Right)).unwrap(); @@ -842,6 +871,7 @@ fn center_and_right_alignment() { #[test] fn tabs_land_on_default_stops() { + crate::test_fonts(); let d = Document::from_text("a\tb"); let l = lay(&d); let xb = l.caret(&Pos::body(0, 2)).unwrap().x; @@ -855,6 +885,7 @@ fn tabs_land_on_default_stops() { /// Letterheads that line up a footer with tabs under a negative indent rely on it. #[test] fn tabs_left_of_the_margin_stay_on_a_default_stop() { + crate::test_fonts(); let x_of = |text: &str, indent: f32, off: usize| { let mut d = Document::from_text(text); d.format_paragraphs(&Pos::body(0, 0), &Pos::body(0, 0), &|p| p.indent_left = Some(indent)).unwrap(); @@ -869,6 +900,7 @@ fn tabs_left_of_the_margin_stay_on_a_default_stop() { #[test] fn right_tab_aligns_text_end() { + crate::test_fonts(); let mut d = Document::from_text("left\tright"); d.format_paragraphs(&Pos::body(0, 0), &Pos::body(0, 0), &|p| { p.tabs = Some(vec![wordcraft_doc::props::TabStop { pos: 400.0, align: wordcraft_doc::props::TabAlign::Right, leader: Default::default() }]) @@ -881,6 +913,7 @@ fn right_tab_aligns_text_end() { #[test] fn lists_get_labels_and_indent() { + crate::test_fonts(); let mut d = Document::from_text("one\ntwo\nthree"); let id = d.numbering.add_list(wordcraft_doc::ListKind::Numbered); d.format_paragraphs(&Pos::body(0, 0), &Pos::body(2, 5), &|p| p.numbering = Some(wordcraft_doc::props::NumRef { num: id, level: 0 })).unwrap(); @@ -897,6 +930,7 @@ fn lists_get_labels_and_indent() { #[test] fn break_only_list_has_no_label() { + crate::test_fonts(); use wordcraft_doc::ListKind; for kind in [ListKind::Numbered, ListKind::Bullet] { let mut d = Document::from_text("First\n\u{000C}\nLast\n\nAfter"); @@ -927,6 +961,7 @@ fn break_only_list_has_no_label() { #[test] fn a_numbered_heading_opening_with_a_page_break_takes_its_number_along() { + crate::test_fonts(); // "Before", then a numbered paragraph whose text opens with a page break: Word draws its // number on the next page with the text, not alone at the bottom of the first. let mut d = Document::from_text("Before\n\u{000C}Heading"); @@ -947,6 +982,7 @@ fn a_numbered_heading_opening_with_a_page_break_takes_its_number_along() { #[test] fn russian_letter_list_labels_draw_cyrillic() { + crate::test_fonts(); // russianLower labelled items 1, 2, 3 instead of а), б), в). use wordcraft_doc::section::NumFormat; let mut d = Document::from_text("один\nдва\nтри"); @@ -978,6 +1014,7 @@ fn russian_letter_list_labels_draw_cyrillic() { #[test] fn chinese_counting_list_labels_and_their_font_runs() { + crate::test_fonts(); // Issue #534: a "一、" list showed digits, drawn with glyph ids of another font. use wordcraft_doc::section::NumFormat; let mut d = Document::from_text("甲\n乙\n丙"); @@ -1027,6 +1064,7 @@ fn chinese_counting_list_labels_and_their_font_runs() { #[test] fn symbol_font_bullet_without_the_font_draws_a_bullet() { + crate::test_fonts(); // Word's default bullet: U+F0B7 in Symbol. let mut d = Document::from_text("one"); let id = d.numbering.add_list(wordcraft_doc::ListKind::BulletChar('\u{F0B7}')); @@ -1045,6 +1083,7 @@ fn symbol_font_bullet_without_the_font_draws_a_bullet() { #[test] fn wingdings_text_draws_and_reads_as_its_unicode_symbols() { + crate::test_fonts(); // Word stores symbol-font text both ways: plain codes ("J" = smiley) and private-use codes // (U+F0FC = check mark, what `w:sym` writes). let mut d = Document::from_text("Hi J\u{F0FC} ok"); @@ -1076,6 +1115,7 @@ fn wingdings_text_draws_and_reads_as_its_unicode_symbols() { #[test] fn tables_lay_out_cells() { + crate::test_fonts(); let mut d = Document::from_text("before\nafter"); let mut t = Table::new(2, 3, 468.0); t.rows[0].cells[1].blocks = vec![wordcraft_doc::para_block(wordcraft_doc::Paragraph::with_text("cell text", Default::default()))]; @@ -1095,6 +1135,7 @@ fn tables_lay_out_cells() { /// continuation row's box is `covered` (no gridline, not a hit). #[test] fn a_vertical_merge_lays_out_as_one_cell() { + crate::test_fonts(); let mut d = Document::from_text("before\nafter"); let mut t = Table::new(3, 2, 468.0); t.rows[0].cells[0].props.vmerge = wordcraft_doc::props::VMerge::Restart; @@ -1137,6 +1178,7 @@ fn turned_lines(l: &DocLayout) -> &Placed { /// grows to fit it, and caret, clicks and drawing follow the turn. #[test] fn turned_cell_text_runs_down_the_cell() { + crate::test_fonts(); let text = "Turned cell text"; let d = turned_table(TextDirection::Down, text, None); let l = lay(&d); @@ -1175,6 +1217,7 @@ fn turned_cell_text_runs_down_the_cell() { #[test] fn turned_up_cell_text_reads_bottom_to_top() { + crate::test_fonts(); let text = "Bottom to top"; let d = turned_table(TextDirection::Up, text, None); let l = lay(&d); @@ -1190,6 +1233,7 @@ fn turned_up_cell_text_reads_bottom_to_top() { #[test] fn turned_text_wraps_in_an_exact_row() { + crate::test_fonts(); let d = turned_table(TextDirection::Down, &"word ".repeat(30), Some(72.0)); let l = lay(&d); let it = turned_lines(&l); @@ -1203,6 +1247,7 @@ fn turned_text_wraps_in_an_exact_row() { #[test] fn hostile_turned_cells_never_panic() { + crate::test_fonts(); // Empty, huge and nested turned cells, in exact rows too small for anything. for dir in [TextDirection::Down, TextDirection::Up] { for row in [None, Some(0.5), Some(1e9)] { @@ -1230,6 +1275,7 @@ fn rules_on_page0(l: &DocLayout) -> Vec { #[test] fn table_style_borders_survive_empty_tbl_borders() { + crate::test_fonts(); let mut d = Document::from_text("before\nafter"); let mut t = Table::new(2, 2, 468.0); // defaults to the TableGrid style t.props.borders = Some(Borders::default()); // what an empty parses to @@ -1247,6 +1293,7 @@ fn table_style_borders_survive_empty_tbl_borders() { /// vertical edge it meets, not at its centre line. #[test] fn table_border_corners_close() { + crate::test_fonts(); let mut d = Document::from_text("before\nafter"); let mut t = Table::new(2, 2, 468.0); let thick = Some(Border::single(3.0)); @@ -1272,6 +1319,7 @@ fn table_border_corners_close() { #[test] fn table_own_border_overrides_only_that_side() { + crate::test_fonts(); let mut d = Document::from_text("before\nafter"); let mut t = Table::new(2, 2, 468.0); t.props.borders = Some(Borders { top: Some(Border::single(2.0)), ..Default::default() }); @@ -1284,6 +1332,7 @@ fn table_own_border_overrides_only_that_side() { #[test] fn table_nil_border_hides_style_side() { + crate::test_fonts(); let mut d = Document::from_text("before\nafter"); let mut t = Table::new(1, 1, 468.0); // 1×1: no inside edges t.props.borders = Some(Borders::all(Border { style: BorderStyle::None, width: 0.0, color: None, space: 0.0 })); @@ -1293,6 +1342,7 @@ fn table_nil_border_hides_style_side() { #[test] fn table_without_style_stays_borderless() { + crate::test_fonts(); let mut d = Document::from_text("before\nafter"); let mut t = Table::new(2, 2, 468.0); t.props.style = None; @@ -1303,6 +1353,7 @@ fn table_without_style_stays_borderless() { #[test] fn headers_and_page_fields() { + crate::test_fonts(); let mut d = Document::from_text(&"para\n".repeat(120)); let mut hp = wordcraft_doc::Paragraph::new(); hp.insert_object( @@ -1327,6 +1378,7 @@ fn headers_and_page_fields() { #[test] fn tall_first_page_header_pushes_body_down() { + crate::test_fonts(); let mut d = Document::from_text(&"para\n".repeat(120)); let tall: Vec<_> = (0..12).map(|_| wordcraft_doc::para_block(wordcraft_doc::Paragraph::new())).collect(); let id = d.add_part(wordcraft_doc::PartKind::Header, tall); @@ -1343,6 +1395,7 @@ fn tall_first_page_header_pushes_body_down() { #[test] fn cache_reuses_unchanged_paragraphs() { + crate::test_fonts(); let d = Document::from_text(&"some text here\n".repeat(50)); let mut c = LayoutCache::new(); layout(&d, &mut c, &LayoutOptions::default()); @@ -1358,6 +1411,7 @@ fn cache_reuses_unchanged_paragraphs() { /// again after an edit elsewhere measures nothing again. #[test] fn cache_keeps_one_layout_per_table_cell() { + crate::test_fonts(); let mut d = Document::from_text("before\nafter"); let mut t = Table::new(4, 3, 468.0); for (r, row) in t.rows.iter_mut().enumerate() { @@ -1378,6 +1432,7 @@ fn cache_keeps_one_layout_per_table_cell() { #[test] fn selection_rects_cover_range() { + crate::test_fonts(); let d = Document::from_text("Hello world\nSecond line"); let l = lay(&d); let r = l.selection_rects(&d, &Pos::body(0, 6), &Pos::body(1, 6), 0); @@ -1387,6 +1442,7 @@ fn selection_rects_cover_range() { #[test] fn column_segments_take_the_same_x_range_on_every_line() { + crate::test_fonts(); let d = Document::from_text("abcdef\nabcdef\nab"); let l = lay(&d); let left = l.caret(&Pos::body(0, 2)).unwrap().x; @@ -1401,6 +1457,7 @@ fn column_segments_take_the_same_x_range_on_every_line() { #[test] fn display_has_glyphs_and_marks() { + crate::test_fonts(); let mut d = Document::from_text("Hello\tworld"); d.format_range(&Pos::body(0, 0), &Pos::body(0, 5), &|c| c.underline = Some(wordcraft_doc::props::Underline::Single)).unwrap(); let l = lay(&d); @@ -1413,6 +1470,7 @@ fn display_has_glyphs_and_marks() { #[test] fn a_tracked_paragraph_mark_shows_in_its_authors_colour() { + crate::test_fonts(); let mut d = Document::from_text("Split\nhere"); d.revisions.push(wordcraft_doc::Revision { kind: wordcraft_doc::RevisionKind::Insert, @@ -1434,6 +1492,7 @@ fn a_tracked_paragraph_mark_shows_in_its_authors_colour() { #[test] fn section_breaks_show_with_formatting_marks() { + crate::test_fonts(); use wordcraft_doc::section::{SectionProps, SectionStart}; // Two sections: "One" ends the first; the second starts as `start`. let doc = |start: SectionStart| { @@ -1462,6 +1521,7 @@ fn section_breaks_show_with_formatting_marks() { #[test] fn space_between_paragraphs_is_the_larger_of_after_and_before() { + crate::test_fonts(); // (space after the first, space before the second, contextual spacing on both) -> gap let gap = |after: f32, before: f32, contextual: bool| -> f32 { let mut d = Document::from_text("One\nTwo"); @@ -1486,6 +1546,7 @@ fn space_between_paragraphs_is_the_larger_of_after_and_before() { #[test] fn space_between_paragraphs_is_their_sum_when_html_auto_spacing_is_off() { + crate::test_fonts(); // Measured in Word (compatibility modes 14 and 15): with `w:doNotUseHTMLParagraphAutoSpacing`, // 12 pt after and 12 pt before leave 24 pt; without it, 12 pt. let gaps = |sum: bool| -> (f32, f32) { @@ -1519,6 +1580,7 @@ fn space_between_paragraphs_is_their_sum_when_html_auto_spacing_is_off() { #[test] fn space_between_paragraphs_in_a_cell_follows_the_body_rule() { + crate::test_fonts(); // Three paragraphs in a cell, each 6pt before and 6pt after -> the gaps between them. let gaps = |contextual: bool| -> Vec { let mut d = Document::from_text("before\nafter"); @@ -1552,6 +1614,7 @@ fn space_between_paragraphs_in_a_cell_follows_the_body_rule() { #[test] fn sections_land_on_the_side_their_break_and_numbering_ask_for() { + crate::test_fonts(); use wordcraft_doc::section::{SectionProps, SectionStart}; // "One" ends the first section on page 1; the second section starts as `start`. let pages = |start: SectionStart, restart: Option, even_odd: bool| -> Vec<(usize, u32)> { @@ -1579,6 +1642,7 @@ fn sections_land_on_the_side_their_break_and_numbering_ask_for() { #[test] fn paragraph_marks_sit_past_text_running_the_other_way() { + crate::test_fonts(); // A left-to-right paragraph ending in Hebrew, and a right-to-left one ending in Latin: the // logically last letters are at the wrong edge, but the ¶ still goes past every glyph. let mut d = Document::from_text("Hello \u{5e9}\u{5dc}\u{5d5}\u{5dd}\n\u{5e9}\u{5dc}\u{5d5}\u{5dd} abc"); @@ -1619,6 +1683,7 @@ fn border_lines_in(d: &Document, l: &DocLayout) -> Vec<(f32, f32, f32, f32)> { #[test] fn character_border_draws_one_box_around_run() { + crate::test_fonts(); use wordcraft_doc::props::Border; let mut d = Document::from_text("Hello world"); assert!(border_lines(&d).is_empty()); @@ -1640,6 +1705,7 @@ fn character_border_draws_one_box_around_run() { #[test] fn equal_adjacent_borders_share_a_box() { + crate::test_fonts(); use wordcraft_doc::props::Border; let mut d = Document::from_text("ab"); d.format_range(&Pos::body(0, 0), &Pos::body(0, 2), &|c| c.border = Some(Border::single(0.5))).unwrap(); @@ -1651,6 +1717,7 @@ fn equal_adjacent_borders_share_a_box() { #[test] fn wrapped_border_boxes_each_line() { + crate::test_fonts(); use wordcraft_doc::props::Border; let text = "boxed ".repeat(20); let mut d = Document::from_text(text.trim_end()); @@ -1662,6 +1729,7 @@ fn wrapped_border_boxes_each_line() { #[test] fn runs_differing_only_in_link_or_decoration_keep_their_own_style() { + crate::test_fonts(); let mut d = Document::from_text("Alpha Beta Gamma Delta"); let set = |d: &mut Document, a: usize, b: usize, f: &dyn Fn(&mut wordcraft_doc::props::CharProps)| { d.format_range(&Pos::body(0, a), &Pos::body(0, b), f).unwrap(); @@ -1698,6 +1766,7 @@ fn runs_differing_only_in_link_or_decoration_keep_their_own_style() { #[test] fn web_view_is_one_page() { + crate::test_fonts(); let d = Document::from_text(&"text ".repeat(3000)); let mut c = LayoutCache::new(); let l = layout(&d, &mut c, &LayoutOptions { view: ViewMode::Web, web_width: 800.0, ..Default::default() }); @@ -1707,6 +1776,7 @@ fn web_view_is_one_page() { #[test] fn hostile_props_do_not_panic() { + crate::test_fonts(); let mut d = Document::from_text("x\ny"); d.format_paragraphs(&Pos::body(0, 0), &Pos::body(1, 0), &|p| { *p = ParaProps { indent_left: Some(1e9), indent_right: Some(1e9), indent_first: Some(-1e9), ..Default::default() } @@ -1720,6 +1790,7 @@ fn hostile_props_do_not_panic() { #[test] fn layout_is_fast() { + crate::test_fonts(); let d = Document::from_text(&"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore.\n".repeat(3000)); let mut c = LayoutCache::new(); @@ -1737,6 +1808,7 @@ fn layout_is_fast() { #[test] fn empty_center_tab_then_right_tab() { + crate::test_fonts(); let mut d = Document::from_text("left\t\tright"); d.format_paragraphs(&Pos::body(0, 0), &Pos::body(0, 0), &|p| p.style = Some("Header".into())).unwrap(); let l = lay(&d); @@ -1746,6 +1818,7 @@ fn empty_center_tab_then_right_tab() { #[test] fn footnotes_sit_at_page_bottom() { + crate::test_fonts(); let mut d = Document::from_text(&"Body text line.\n".repeat(30)); let mut note = wordcraft_doc::Paragraph::with_text("The note text.", Default::default()).styled("FootnoteText"); note.props.space_after = Some(0.0); @@ -1781,6 +1854,7 @@ fn footnotes_sit_at_page_bottom() { #[test] fn deleted_note_anchors_take_no_number_and_print_no_note() { + crate::test_fonts(); use wordcraft_doc::para::NoteKind; let mut d = Document::from_text("Alpha Beta Gamma"); let mut ids = Vec::new(); @@ -1827,6 +1901,7 @@ fn deleted_note_anchors_take_no_number_and_print_no_note() { #[test] fn hidden_note_anchors_keep_their_number_and_their_note() { + crate::test_fonts(); // Measured in Word (hidden text not printed): a hidden footnote or endnote reference mark still // takes its number, so the visible footnote after it is 2, and both notes are still printed. use wordcraft_doc::para::NoteKind; @@ -1873,6 +1948,7 @@ fn hidden_note_anchors_keep_their_number_and_their_note() { #[test] fn text_wraps_around_square_float() { + crate::test_fonts(); let mut d = Document::from_text(&"Words flow around the picture here. ".repeat(30)); let float = wordcraft_doc::para::Float { wrap: wordcraft_doc::para::Wrap::Square, @@ -1909,6 +1985,7 @@ fn text_wraps_around_square_float() { #[test] fn freeform_arrow_end_draws_at_the_last_point() { + crate::test_fonts(); use std::sync::Arc; use wordcraft_doc::connector::ShapeExtra; use wordcraft_doc::freeform::{FreePath, Freeform}; @@ -1954,6 +2031,7 @@ fn freeform_arrow_end_draws_at_the_last_point() { #[test] fn hidden_drawings_are_neither_drawn_nor_take_room() { + crate::test_fonts(); // A hidden floating shape (VML `visibility:hidden`, `wp:docPr/@hidden`): no wrap area, nothing // drawn, as in Word for Windows. A hidden inline one takes no room in its line. let mut d = Document::from_text(&"Words flow around the picture here. ".repeat(30)); @@ -2004,6 +2082,7 @@ fn hidden_drawings_are_neither_drawn_nor_take_room() { #[test] fn deleted_float_leaves_no_wrap_area_without_markup() { + crate::test_fonts(); // A square-wrapped shape anchored at the start of the paragraph, its anchor track-deleted. let text = "Words flow around the picture here. ".repeat(30); let mut d = Document::from_text(&text); @@ -2050,6 +2129,7 @@ fn deleted_float_leaves_no_wrap_area_without_markup() { #[test] fn hidden_float_leaves_no_wrap_area_when_hidden_text_is_not_shown() { + crate::test_fonts(); // A square-wrapped shape whose anchor is hidden text, through a character style (so only the // resolved formatting says so). Measured in Word: not printed, and the text runs full width. let text = "Words flow around the picture here. ".repeat(30); @@ -2106,6 +2186,7 @@ fn hidden_float_leaves_no_wrap_area_when_hidden_text_is_not_shown() { /// overflowed when the next page or line was counted: a panic with overflow checks on. #[test] fn extreme_page_and_line_number_starts_do_not_overflow() { + crate::test_fonts(); let mut d = Document::from_text("one\ntwo\nthree"); for b in d.body.iter_mut() { if let wordcraft_doc::Block::Para(p) = std::sync::Arc::make_mut(b) { @@ -2120,6 +2201,7 @@ fn extreme_page_and_line_number_starts_do_not_overflow() { #[test] fn line_numbers_borders_text_boxes() { + crate::test_fonts(); let mut d = Document::from_text("one\ntwo\nthree"); d.last_section.line_numbers = Some(Default::default()); d.last_section.page_borders = Some(wordcraft_doc::props::Borders::box_(wordcraft_doc::props::Border::single(1.0))); @@ -2151,6 +2233,7 @@ fn line_numbers_borders_text_boxes() { #[test] fn tall_rows_split_across_pages() { + crate::test_fonts(); let mut d = Document::from_text("before\nafter"); let mut t = Table::new(2, 2, 468.0); let long = "Row text that keeps going and going so the cell grows taller than a page. ".repeat(120); @@ -2257,6 +2340,7 @@ fn cell_line_bottoms(paras: &[(&str, f32)], i: usize) -> Vec { /// cell paragraph breaks 6 + 2). It used to move the whole row to the next page. #[test] fn a_split_row_keeps_its_bottom_margin_and_widow_control() { + crate::test_fonts(); let text = "Dotted leader lines to fill in by hand later on. ".repeat(14); let paras = [(text.as_str(), 0.0)]; let bottoms = cell_line_bottoms(¶s, 0); @@ -2283,6 +2367,7 @@ fn a_split_row_keeps_its_bottom_margin_and_widow_control() { /// (Word keeps the "Remarks:" line on the page) instead of moving whole. #[test] fn a_split_row_keeps_the_last_paragraphs_space_after() { + crate::test_fonts(); let paras = [("Remarks:", 0.0), ("", 8.0)]; let bottom = cell_line_bottoms(¶s, 1).last().copied().unwrap_or(0.0); // The empty paragraph's line ends 6pt above the page bottom (720): 8pt after + 4pt margin @@ -2297,6 +2382,7 @@ fn a_split_row_keeps_the_last_paragraphs_space_after() { /// continuation is as tall as the cell needing the most room. #[test] fn split_row_cells_each_continue_at_the_top() { + crate::test_fonts(); let text = "Dotted leader lines to fill in by hand later on. ".repeat(14); let row = |push: f32, singles: usize| { let mut d = Document::from_text("Filler"); @@ -2357,6 +2443,7 @@ fn split_row_cells_each_continue_at_the_top() { /// room is less than its minimum height. #[test] fn exact_rows_move_on_whole() { + crate::test_fonts(); let text = "Dotted leader lines to fill in by hand later on. ".repeat(14); let lay_row = |rule: wordcraft_doc::props::HeightRule| { let mut d = Document::from_text("Filler"); @@ -2397,6 +2484,7 @@ fn exact_rows_move_on_whole() { /// which squashed the nested cell to 2pt on the next page. #[test] fn a_nested_cell_cut_by_the_page_bottom_keeps_its_rest() { + crate::test_fonts(); let mut d = Document::from_text("Filler"); if let Some(wordcraft_doc::Block::Para(p)) = d.body.first_mut().map(std::sync::Arc::make_mut) { p.props.space_before = Some(400.0); @@ -2444,6 +2532,7 @@ fn a_nested_cell_cut_by_the_page_bottom_keeps_its_rest() { /// behaviour). #[test] fn a_split_at_least_row_keeps_its_height() { + crate::test_fonts(); let mut d = Document::from_text("Filler"); let mut t = Table::new(1, 1, 400.0); t.rows[0].props.height = Some(1200.0); @@ -2468,6 +2557,7 @@ fn a_split_at_least_row_keeps_its_height() { /// on a NaN cut). #[test] fn split_row_rejects_a_hostile_cut() { + crate::test_fonts(); use crate::table::{CellPart, RowLayout, split_row}; let d = Document::from_text("Line"); let l = lay(&d); @@ -2500,6 +2590,7 @@ fn split_row_rejects_a_hostile_cut() { #[test] fn repeated_header_is_followed_by_the_next_row_even_when_it_overflows() { + crate::test_fonts(); // A one-column table at the top of a letter page (body 648 pt), then an empty paragraph. Returns // the table rows on each page, in order. let rows_per_page = |t: Table| -> Vec> { @@ -2547,6 +2638,7 @@ fn repeated_header_is_followed_by_the_next_row_even_when_it_overflows() { #[test] fn repeated_header_moves_with_the_first_row_that_does_not_fit() { + crate::test_fonts(); // A paragraph that leaves `room` points of the letter page's 648 pt body, then a table with a // 20 pt header row and 40 pt one-line rows. Returns the table rows on each page. let rows_per_page = |room: f32| -> Vec> { @@ -2574,6 +2666,7 @@ fn repeated_header_moves_with_the_first_row_that_does_not_fit() { /// A drop-cap paragraph in a font larger than 1000 pt used to panic in layout (`clamp` with min > max). #[test] fn drop_cap_with_a_huge_font_lays_out() { + crate::test_fonts(); for size in [999.0, 1000.0, 1001.0, 1638.0] { let cp = wordcraft_doc::CharProps { size: Some(size), ..Default::default() }; let mut p = wordcraft_doc::Paragraph::with_text("Hello drop cap world", cp); @@ -2591,6 +2684,7 @@ fn drop_cap_with_a_huge_font_lays_out() { #[test] fn drop_cap_indents_its_lines() { + crate::test_fonts(); let mut p = wordcraft_doc::Paragraph::with_text(&"Every line of this paragraph wraps around a large first letter. ".repeat(6), Default::default()); p.props.drop_cap = Some(3); @@ -2619,6 +2713,7 @@ fn drop_cap_indents_its_lines() { #[test] fn auto_hyphenation_breaks_long_words() { + crate::test_fonts(); let text = "Internationalization considerations notwithstanding, administrators systematically reconsidered extraordinarily uncharacteristic responsibilities. ".repeat(4); let mut d = Document::from_text(&text); let off = lay(&d); @@ -2652,6 +2747,7 @@ fn auto_hyphenation_breaks_long_words() { /// with a hyphen, and words in capitals stay whole when the document says so. #[test] fn hyphenation_options_limit_hyphens_and_spare_capitals() { + crate::test_fonts(); let text = "Internationalization considerations notwithstanding, administrators systematically reconsidered extraordinarily uncharacteristic responsibilities. ".repeat(6); let lines = |d: &Document| -> Vec { lay(d).pages[0] @@ -2706,6 +2802,7 @@ fn cell_para(l: &DocLayout, row: u32, col: u32) -> Arc { /// 8 pt after and 1.15 lines. #[test] fn table_style_paragraph_props_beat_document_defaults() { + crate::test_fonts(); let d = styled_table_doc(Some("TableGrid"), 3); let plain = styled_table_doc(None, 3); let (lg, lp) = (lay(&d), lay(&plain)); @@ -2722,6 +2819,7 @@ fn table_style_paragraph_props_beat_document_defaults() { /// table style only fills in what they leave unset. #[test] fn paragraph_style_and_direct_formatting_beat_table_style() { + crate::test_fonts(); let mut d = styled_table_doc(Some("TableGrid"), 3); d.styles.upsert(wordcraft_doc::Style { id: "Spaced".into(), @@ -2759,6 +2857,7 @@ fn paragraph_style_and_direct_formatting_beat_table_style() { /// adds its own run formatting and conditional formats. #[test] fn derived_table_style_merges_its_base() { + crate::test_fonts(); use wordcraft_doc::styles::TableStyleParts; let red = wordcraft_doc::Rgb(0xC0, 0, 0); let mut d = styled_table_doc(Some("RedGrid"), 3); @@ -2825,6 +2924,7 @@ fn box_rect(l: &DocLayout, id: u32) -> wordcraft_geom::Rect { #[test] fn clicks_inside_a_text_box_hit_its_story() { + crate::test_fonts(); let mut d = Document::from_text("Before after"); let id = text_box(&mut d, 7, "Inside the box", 144.0, 72.0, Default::default()); let l = lay(&d); @@ -2848,6 +2948,7 @@ fn clicks_inside_a_text_box_hit_its_story() { #[test] fn body_text_wins_over_a_text_box_behind_it() { + crate::test_fonts(); let mut d = Document::from_text("Hi"); let float = wordcraft_doc::para::Float { wrap: wordcraft_doc::para::Wrap::BehindText, @@ -2869,6 +2970,7 @@ fn body_text_wins_over_a_text_box_behind_it() { #[test] fn presses_grab_pictures_anywhere_and_text_boxes_by_their_border() { + crate::test_fonts(); let mut d = Document::from_text("Words before the objects."); let id = text_box(&mut d, 0, "Inside", 144.0, 72.0, Default::default()); let shape = InlineObject::Shape { @@ -2924,6 +3026,7 @@ fn box_lines(l: &DocLayout, id: u32) -> (usize, usize, wordcraft_geom::Rect) { #[test] fn text_box_hides_text_that_does_not_fit() { + crate::test_fonts(); let long = "The quick brown fox jumps over the lazy dog. ".repeat(12); let mut d = Document::from_text("Body"); let id = text_box(&mut d, 0, &long, 144.0, 72.0, Default::default()); @@ -2944,6 +3047,7 @@ fn text_box_hides_text_that_does_not_fit() { #[test] fn square_wrap_flows_text_on_both_sides() { + crate::test_fonts(); let float = |x: f32| wordcraft_doc::para::Float { wrap: wordcraft_doc::para::Wrap::Square, h_rel: wordcraft_doc::para::Anchor::Column, @@ -2994,6 +3098,7 @@ fn square_wrap_flows_text_on_both_sides() { /// still use it, and a word wider than the whole line still breaks where it is. #[test] fn a_word_too_wide_for_the_strip_beside_a_float_moves_on_whole() { + crate::test_fonts(); use wordcraft_doc::para::{Anchor, Float, ShapeKind, Wrap}; // A `w` x 150pt square at `x` in the 468pt column, leaving a 32pt strip on its right. Each // line: (top, left, beside the object's far side, ends inside a word, where its text ends). @@ -3076,6 +3181,7 @@ fn page_float(x: f32, y: f32) -> wordcraft_doc::para::Float { #[test] fn text_boxes_show_in_headers_footers_cells_and_notes() { + crate::test_fonts(); let mut d = Document::from_text(&"Body paragraph.\n".repeat(4)); // Header: an inline box and a box placed on the page. let hid = @@ -3119,6 +3225,7 @@ fn text_boxes_show_in_headers_footers_cells_and_notes() { #[test] fn footnotes_inside_text_boxes_are_numbered_and_placed() { + crate::test_fonts(); let mut d = Document::from_text(&"Body text line.\n".repeat(30)); let id = text_box_at(&mut d, &Pos::body(2, 0), "Box text", 144.0, 40.0, Default::default()); let e = d.end_of(StoryRef::Part(id)); @@ -3172,6 +3279,7 @@ fn box_fan_out(levels: usize, fan: usize) -> Document { #[test] fn self_showing_and_fanned_out_text_boxes_stay_bounded() { + crate::test_fonts(); // A box whose 30 shapes all show itself: laid out once. assert_eq!(lay(&box_fan_out(1, 30)).text_boxes, 1); // Chains of boxes each showing the next 30 times (30^4 expansions unbounded). @@ -3186,6 +3294,7 @@ fn self_showing_and_fanned_out_text_boxes_stay_bounded() { #[test] fn many_text_boxes_and_a_header_box_all_show() { + crate::test_fonts(); // Outermost boxes aren't budgeted: a long document's boxes, and its header's on every page, // all get their text. let mut d = Document::from_text("Paragraph with a box."); @@ -3212,6 +3321,7 @@ fn many_text_boxes_and_a_header_box_all_show() { #[test] fn a_text_box_inside_a_text_box_still_shows_its_text() { + crate::test_fonts(); let mut d = Document::from_text("Body"); let outer = text_box(&mut d, 0, "Outer ", 300.0, 200.0, Default::default()); let e = d.end_of(StoryRef::Part(outer)); @@ -3239,6 +3349,7 @@ fn picture(w: f32, h: f32, float: wordcraft_doc::para::Float) -> InlineObject { #[test] fn line_spacing_does_not_scale_pictures() { + crate::test_fonts(); use wordcraft_doc::props::LineSpacing; let mut d = Document::from_text(""); if let Some(wordcraft_doc::Block::Para(p)) = d.body.get_mut(0).map(std::sync::Arc::make_mut) { @@ -3256,6 +3367,7 @@ fn line_spacing_does_not_scale_pictures() { #[test] fn multiple_line_spacing_adds_its_leading_below_the_text() { + crate::test_fonts(); use wordcraft_doc::props::LineSpacing; // (spacing, expected baseline below the line top as a share of the ascent) for (spacing, share) in [(LineSpacing::Multiple(1.15), 1.0), (LineSpacing::Multiple(2.0), 1.0), (LineSpacing::Multiple(0.8), 0.8)] { @@ -3290,6 +3402,7 @@ fn multiple_line_spacing_adds_its_leading_below_the_text() { #[test] fn list_label_does_not_inflate_an_inline_picture_floor() { + crate::test_fonts(); use wordcraft_doc::props::{LineSpacing, NumRef}; for spacing in [LineSpacing::Multiple(1.25), LineSpacing::AtLeast(12.0)] { let mut d = Document::from_text(""); @@ -3310,6 +3423,7 @@ fn list_label_does_not_inflate_an_inline_picture_floor() { #[test] fn body_starts_right_below_a_tall_header() { + crate::test_fonts(); let mut d = Document::from_text("Body"); let mut hp = wordcraft_doc::Paragraph::with_text("", Default::default()); hp.insert_object(0, picture(100.0, 80.0, Default::default()), &Default::default()).unwrap(); @@ -3327,6 +3441,7 @@ fn body_starts_right_below_a_tall_header() { #[test] fn inline_picture_keeps_room_for_its_effects() { + crate::test_fonts(); let mut d = Document::from_text(""); let shadow = wordcraft_doc::para::Float { effect: [6.0, 12.0, 18.0, 27.0], ..Default::default() }; if let Some(wordcraft_doc::Block::Para(p)) = d.body.get_mut(0).map(std::sync::Arc::make_mut) { @@ -3349,6 +3464,7 @@ fn line_tops(l: &DocLayout) -> Vec { #[test] fn at_least_rows_add_cell_margins_and_border_bands() { + crate::test_fonts(); use wordcraft_doc::props::{Border, BorderStyle, Borders, HeightRule}; let mut d = Document::from_text(""); let line = Some(Border { style: BorderStyle::Single, width: 0.5, color: None, space: 0.0 }); @@ -3372,6 +3488,7 @@ fn at_least_rows_add_cell_margins_and_border_bands() { /// own lines up with a neighbour that has them, and the row is as tall as if all had them. #[test] fn a_row_shares_its_largest_top_and_bottom_margins() { + crate::test_fonts(); let mut d = Document::from_text(""); let mut t = Table::new(3, 2, 300.0); t.props.cell_margins = Some([0.0, 5.4, 0.0, 5.4]); @@ -3397,6 +3514,7 @@ fn a_row_shares_its_largest_top_and_bottom_margins() { /// and bottom margins steps 39pt; the text starts 4pt below its top). #[test] fn exact_rows_add_only_the_bottom_margin() { + crate::test_fonts(); use wordcraft_doc::props::HeightRule; let mut d = Document::from_text(""); let mut t = Table::new(2, 1, 200.0); @@ -3416,6 +3534,7 @@ fn exact_rows_add_only_the_bottom_margin() { /// each cell's text starts its left margin inside the cell. #[test] fn cell_margins_are_where_cell_text_starts() { + crate::test_fonts(); let mut d = Document::from_text(""); let mut t = Table::new(1, 2, 300.0); t.props.cell_margins = Some([0.0, 9.0, 0.0, 3.0]); @@ -3467,6 +3586,7 @@ fn justified_lines(text: &str, compat_mode: u32, align: Align) -> (usize, Vec f32 { #[test] fn floats_align_within_their_reference_area() { + crate::test_fonts(); use wordcraft_doc::para::{Anchor, Float, FloatAlign, Wrap}; let mut d = Document::from_text("Some text beside the pictures."); let centred = Float { wrap: Wrap::InFrontOfText, h_rel: Anchor::Margin, h_align: Some(FloatAlign::Center), x: 999.0, ..Default::default() }; @@ -3549,6 +3672,7 @@ fn floats_align_within_their_reference_area() { #[test] fn top_and_bottom_float_pushes_its_own_first_line_down() { + crate::test_fonts(); use wordcraft_doc::para::{Anchor, Float, Wrap}; let mut d = Document::from_text(&"Text that starts below the picture. ".repeat(10)); let f = Float { wrap: Wrap::TopAndBottom, h_rel: Anchor::Column, v_rel: Anchor::Paragraph, dist_bottom: 6.0, ..Default::default() }; @@ -3564,6 +3688,7 @@ fn top_and_bottom_float_pushes_its_own_first_line_down() { #[test] fn float_follows_its_paragraph_to_the_next_page() { + crate::test_fonts(); use wordcraft_doc::para::{Anchor, Float, Wrap}; let mut d = Document::from_text(&format!("First page.\n{}", "Text beside the picture on page two. ".repeat(20))); let f = Float { wrap: Wrap::Square, h_rel: Anchor::Column, v_rel: Anchor::Paragraph, dist: 9.0, ..Default::default() }; @@ -3585,6 +3710,7 @@ fn float_follows_its_paragraph_to_the_next_page() { /// break and then the page-anchored text box with the title; the box stays on the cover. #[test] fn a_float_anchored_after_a_page_break_stays_on_its_paragraph_s_first_page() { + crate::test_fonts(); let mut d = Document::from_text(&format!("{}\nAfter the cover", wordcraft_doc::para::PAGE_BREAK)); d.insert_object(&Pos::body(0, 1), rect_shape(200.0, 100.0, page_float(100.0, 300.0)), &Default::default()).unwrap(); let l = lay(&d); @@ -3597,6 +3723,7 @@ fn a_float_anchored_after_a_page_break_stays_on_its_paragraph_s_first_page() { #[test] fn floats_in_headers_and_table_cells_are_drawn() { + crate::test_fonts(); use wordcraft_doc::para::{Anchor, Float, Wrap}; let mut d = Document::from_text("Body text."); let logo = Float { wrap: Wrap::BehindText, h_rel: Anchor::Page, v_rel: Anchor::Page, x: 400.0, y: 20.0, ..Default::default() }; @@ -3622,6 +3749,7 @@ fn floats_in_headers_and_table_cells_are_drawn() { #[test] fn wrap_area_uses_each_distance() { + crate::test_fonts(); use wordcraft_doc::para::{Float, Wrap}; let f = Float { wrap: Wrap::Square, dist: 9.0, dist_top: 0.0, dist_bottom: 4.0, ..Default::default() }; let (r, tb, ..) = wrap_area(Rect::new(10.0, 20.0, 100.0, 50.0), &f, None).unwrap(); @@ -3634,6 +3762,7 @@ fn wrap_area_uses_each_distance() { #[test] fn float_moved_to_a_new_page_sits_at_its_paragraph_top() { + crate::test_fonts(); use wordcraft_doc::para::{Anchor, Float, Wrap}; let mut d = Document::from_text(&format!("First page.\n{}", "Text beside the picture on page two. ".repeat(20))); let f = Float { wrap: Wrap::Square, h_rel: Anchor::Column, v_rel: Anchor::Paragraph, dist: 9.0, ..Default::default() }; @@ -3653,6 +3782,7 @@ fn float_moved_to_a_new_page_sits_at_its_paragraph_top() { #[test] fn page_relative_header_float_wraps_where_it_is_drawn() { + crate::test_fonts(); use wordcraft_doc::para::{Anchor, Float, Wrap}; let body_top = |wrap| { let mut d = Document::from_text("Body text."); @@ -3674,6 +3804,7 @@ fn page_relative_header_float_wraps_where_it_is_drawn() { /// at the top margin and paginates exactly as without the box. #[test] fn floating_header_text_box_leaves_the_body_at_the_top_margin() { + crate::test_fonts(); use wordcraft_doc::para::{Anchor, Float, ShapeKind, Wrap}; let body: Vec = (1..=40).map(|i| format!("Body paragraph {i}: synthetic public test content.")).collect(); let make = |float: Option| { @@ -3745,6 +3876,7 @@ fn numbered(d: &mut Document, texts: &[&str], props: ParaProps) -> Vec = (0..24).map(|i| format!("Paragraph {i} before the table.")).collect(); @@ -3920,6 +4059,7 @@ fn a_page_anchored_floating_table_after_a_full_page_moves_to_the_next() { /// on that page: no blank page between the two tables around it. #[test] fn a_page_break_at_the_bottom_of_a_full_page_leaves_no_blank_page() { + crate::test_fonts(); use wordcraft_doc::props::LineSpacing; let props = ParaProps { line_spacing: Some(LineSpacing::Exactly(24.0)), space_before: Some(0.0), space_after: Some(0.0), ..Default::default() }; let para = |text: &str| { @@ -3941,6 +4081,7 @@ fn a_page_break_at_the_bottom_of_a_full_page_leaves_no_blank_page() { /// size and win over the indent in points. #[test] fn character_indents_use_the_paragraph_mark_size() { + crate::test_fonts(); let mut p = wordcraft_doc::Paragraph::with_text(&"字".repeat(80), Default::default()); p.props.indent_first = Some(5.0); p.props.indent_first_chars = Some(2.0); @@ -3956,6 +4097,7 @@ fn character_indents_use_the_paragraph_mark_size() { #[test] fn no_line_break_right_after_a_slash() { + crate::test_fonts(); // Word keeps "and/or" and web addresses whole on a line when they fit. let text = "word and/or https://example.org/keepers/log/winter ".repeat(40); let d = Document::from_text(&text); @@ -3976,6 +4118,7 @@ fn no_line_break_right_after_a_slash() { #[test] fn unequal_formats_lay_out_in_linear_time() { + crate::test_fonts(); // Runs whose formats never compare equal (NaN spacing from a hostile file) each get a style; // finding a run's style must not compare it against every earlier one. let doc = |n: usize| { @@ -3998,6 +4141,7 @@ fn unequal_formats_lay_out_in_linear_time() { #[test] fn formats_past_65536_in_a_paragraph_keep_their_own_style() { + crate::test_fonts(); // More distinct formats than a 16-bit index counts (the paragraph mark takes a style too): every // run past that limit still draws with its own link, deletion and size, not an earlier run's. let n = usize::from(u16::MAX) + 3; @@ -4030,6 +4174,7 @@ fn formats_past_65536_in_a_paragraph_keep_their_own_style() { #[test] fn hidden_float_in_a_table_cell_takes_no_room() { + crate::test_fonts(); use wordcraft_doc::para::{Anchor, Float, Wrap}; // A square-wrapped shape anchored in hidden text inside a table cell, beside wrapping text. let mut d = Document::from_text("Body text."); @@ -4057,6 +4202,7 @@ fn hidden_float_in_a_table_cell_takes_no_room() { #[test] fn objects_left_out_through_the_table_style_formatting() { + crate::test_fonts(); // The table style's character formatting sits under the runs' own, as the cell is laid out. let mut p = wordcraft_doc::Paragraph::with_text("ab", Default::default()); p.insert_object(1, rect_shape(10.0, 10.0, Default::default()), &Default::default()).unwrap(); @@ -4069,6 +4215,7 @@ fn objects_left_out_through_the_table_style_formatting() { #[test] fn hidden_float_in_a_text_box_is_not_placed() { + crate::test_fonts(); use wordcraft_doc::para::{Anchor, Float, Wrap}; // A shape anchored in hidden text inside a text box: no drawing, no hit area. let mut d = Document::from_text("Body text."); @@ -4090,6 +4237,7 @@ fn hidden_float_in_a_text_box_is_not_placed() { #[test] fn inline_graphic_reserves_its_height() { + crate::test_fonts(); let mut d = Document::from_text("Chart\nAfter"); let obj = InlineObject::Graphic { w: 216.0, h: 144.0, alt: String::new(), float: Default::default(), graphic: Arc::new(Default::default()) }; d.insert_object(&Pos::body(0, 0), obj, &Default::default()).unwrap(); @@ -4101,6 +4249,7 @@ fn inline_graphic_reserves_its_height() { #[test] fn graphic_items_draw_inside_the_object() { + crate::test_fonts(); use wordcraft_doc::graphic::{Graphic, GraphicItem, GraphicKind, PathSeg, TextAlign}; use wordcraft_doc::props::Rgb; let items = vec![ @@ -4144,6 +4293,7 @@ fn graphic_items_draw_inside_the_object() { #[test] fn graphic_text_in_a_degenerate_rectangle_is_not_drawn() { + crate::test_fonts(); use wordcraft_doc::graphic::{Graphic, GraphicItem, GraphicKind, TextAlign}; use wordcraft_doc::props::Rgb; let text = |rect: [f32; 4], size: f32| GraphicItem::Text { @@ -4171,6 +4321,7 @@ fn flat(draws: Vec) -> Vec { #[test] fn inline_chart_is_a_figure_with_its_alt_text() { + crate::test_fonts(); use wordcraft_doc::graphic::{Graphic, GraphicItem, GraphicKind, PathSeg}; use wordcraft_doc::props::Rgb; let items = vec![GraphicItem::Path { @@ -4190,6 +4341,7 @@ fn inline_chart_is_a_figure_with_its_alt_text() { #[test] fn graphic_paths_drop_segments_that_cannot_be_drawn() { + crate::test_fonts(); use wordcraft_doc::graphic::{Graphic, GraphicItem, GraphicKind, PathSeg}; use wordcraft_doc::props::Rgb; // The line before the first move, the move with a NaN, the infinite line and the line after the @@ -4216,6 +4368,7 @@ fn graphic_paths_drop_segments_that_cannot_be_drawn() { #[test] fn graphic_items_scale_with_their_frame() { + crate::test_fonts(); use wordcraft_doc::graphic::{Graphic, GraphicItem, GraphicKind}; use wordcraft_doc::props::Rgb; // A chart drawn for 1000 x 500 pt in a 500 x 250 pt frame: its items and strokes scale by half. @@ -4241,6 +4394,7 @@ fn graphic_items_scale_with_their_frame() { #[test] fn inline_picture_wider_than_a_text_box_is_fitted_to_it() { + crate::test_fonts(); // Text boxes don't clip what runs past their sides, so a 400 x 100 pt picture in a 144 pt box // is fitted to the box's 129.6 pt inside its margins instead of spilling over the page. let mut d = Document::from_text("Before"); @@ -4275,6 +4429,7 @@ fn inline_picture_wider_than_a_text_box_is_fitted_to_it() { #[test] fn inline_picture_wider_than_the_text_keeps_its_size() { + crate::test_fonts(); // A 1000 x 100 pt picture in 468 pt of text: Word draws it at its size, past the margin, and the // page edge clips it; the line is as tall as the picture (docxide-pdf's case179). let mut d = Document::from_text(""); @@ -4292,6 +4447,7 @@ fn inline_picture_wider_than_the_text_keeps_its_size() { #[test] fn a_picture_too_wide_after_a_content_control_start_stays_on_the_first_line() { + crate::test_fonts(); // The picture opening a picture content control (a logo in a text box a little too // narrow for it): the control's start marker before it takes no room, so the picture is // still the line's first item and stays there instead of leaving an empty line above it. @@ -4322,6 +4478,7 @@ fn para_at(l: &DocLayout, path: &[u32]) -> Vec<(f32, f32, usize)> { #[test] fn a_floating_table_in_a_cell_takes_no_room_and_the_cell_holds_it() { + crate::test_fonts(); use wordcraft_doc::para::Anchor; use wordcraft_doc::props::TableFloat; let text = |s: &str| wordcraft_doc::para_block(wordcraft_doc::Paragraph::with_text(s, Default::default())); @@ -4349,6 +4506,7 @@ fn a_floating_table_in_a_cell_takes_no_room_and_the_cell_holds_it() { #[test] fn autofit_sizes_columns_by_their_content() { + crate::test_fonts(); let text = |s: &str| wordcraft_doc::para_block(wordcraft_doc::Paragraph::with_text(s, Default::default())); // No width anywhere: the columns come from their content. let mut t = Table::new(1, 2, 468.0); @@ -4373,6 +4531,7 @@ fn autofit_sizes_columns_by_their_content() { #[test] fn autofit_handles_spanning_cells_long_words_and_empty_tables() { + crate::test_fonts(); let text = |s: &str| wordcraft_doc::para_block(wordcraft_doc::Paragraph::with_text(s, Default::default())); let table_x = |t: Table, path: &[u32]| { let mut d = Document::from_text("After"); @@ -4414,6 +4573,7 @@ fn autofit_handles_spanning_cells_long_words_and_empty_tables() { #[test] fn an_inherited_footer_sizes_its_percentage_table_by_its_own_section() { + crate::test_fonts(); use wordcraft_doc::section::SectionProps; // Measured in Word: a 100%-wide footer table defined by a portrait section keeps that width on // a landscape page that inherits the footer, centred there; a landscape section naming the @@ -4445,19 +4605,19 @@ fn an_inherited_footer_sizes_its_percentage_table_by_its_own_section() { #[test] fn a_row_with_less_room_than_its_minimum_height_moves_on_whole() { + crate::test_fonts(); use wordcraft_doc::props::HeightRule; // Measured in Word: with 49 pt left on the page, a row of eight lines splits, but the same row // with an at-least height of 63.6 pt goes to the next page whole. let first_row_line_page = |min: Option| { - let lines: Vec = (1..=200).map(|i| format!("Filler {i}")).collect(); - let one_page = lay(&Document::from_text(&lines.join("\n"))).pages[0].items.iter().filter(|i| matches!(i, Placed::Lines { .. })).count(); let mut t = Table::new(1, 1, 400.0); t.rows[0].props.height = min; t.rows[0].props.height_rule = HeightRule::AtLeast; t.rows[0].cells[0].blocks = (1..=8).map(|k| wordcraft_doc::para_block(wordcraft_doc::Paragraph::with_text(&format!("Row line {k}"), Default::default()))).collect(); - // Fill the page, leaving two filler paragraphs' room (less than 63.6 pt) for the row. - let mut d = Document::from_text(&lines[..one_page - 2].join("\n")); + // With the pinned Source Sans 3 fallback, 23 Normal paragraphs leave 43.13 pt. + let lines: Vec = (1..=23).map(|i| format!("Filler {i}")).collect(); + let mut d = Document::from_text(&lines.join("\n")); let end = d.body.len(); d.insert_block(StoryRef::Body, &Path::top(end), wordcraft_doc::Block::Table(t)).unwrap(); let l = lay(&d); @@ -4469,6 +4629,7 @@ fn a_row_with_less_room_than_its_minimum_height_moves_on_whole() { #[test] fn text_overflowing_an_exact_row_is_clipped_to_it() { + crate::test_fonts(); use wordcraft_doc::props::HeightRule; // An exactly 12 pt row holding three lines: Word shows what fits and cuts the rest at the row's // edge, rather than drawing it over the rows below. @@ -4495,6 +4656,7 @@ fn text_overflowing_an_exact_row_is_clipped_to_it() { #[test] fn a_percentage_width_past_the_text_column_is_kept() { + crate::test_fonts(); // `w:tblW w:type="pct" w:w="6564"` (131.28%) with the left margin as a negative indent: a // cover-page footer table that spans the page edge to edge. let mut t = Table::new(1, 2, 234.0); @@ -4512,6 +4674,7 @@ fn a_percentage_width_past_the_text_column_is_kept() { #[test] fn a_floating_table_in_the_footer_leaves_the_footer_where_it_is() { + crate::test_fonts(); use wordcraft_doc::para::Anchor; use wordcraft_doc::props::TableFloat; let footer_y = |float: bool| { @@ -4536,6 +4699,7 @@ fn a_floating_table_in_the_footer_leaves_the_footer_where_it_is() { #[test] fn a_split_row_moves_shading_below_the_cut_with_it() { + crate::test_fonts(); use crate::table::{CellPart, RowLayout, split_row}; let line_row = |y: f32| { let d = Document::from_text("Line"); @@ -4566,6 +4730,7 @@ fn a_split_row_moves_shading_below_the_cut_with_it() { #[test] fn kinsoku_keeps_a_manual_line_break_before_a_closing_bracket() { + crate::test_fonts(); // #269: kinsoku forbids a line starting with 」, but a manual line break (Shift+Enter) right // before one still ends the line there. let text = "日本語\n」テスト"; @@ -4591,6 +4756,7 @@ fn kinsoku_keeps_a_manual_line_break_before_a_closing_bracket() { /// testing follows the turned shape, not its frame. #[test] fn rotated_float_wraps_around_its_rotated_bounds() { + crate::test_fonts(); let text = "Words flow around the turned shape here. ".repeat(30); let place = |rot: f32| { let mut d = Document::from_text(&text); @@ -4646,6 +4812,7 @@ fn rotated_float_wraps_around_its_rotated_bounds() { /// its page direction inside the turn; one that does turns with the shape. #[test] fn rotated_group_turns_its_members_and_their_shadows() { + crate::test_fonts(); use wordcraft_doc::effects::{Shadow, ShapeEffects}; use wordcraft_doc::para::{Anchor, Float, GroupChild, ShapeKind, Wrap}; let member = |rot_with_shape: bool, rot: f32| InlineObject::Shape { @@ -4734,6 +4901,7 @@ fn rules_below(l: &DocLayout, pi: usize, y: f32) -> Vec { #[test] fn long_footnotes_continue_on_the_next_page() { + crate::test_fonts(); let mut d = Document::from_text(&"Body text line.\n".repeat(16)); let long = footnote(&mut d, &Pos::body(4, 4), &"A long footnote that runs on and on. ".repeat(100)); let own = footnote(&mut d, &Pos::body(14, 4), "The second page's own note."); @@ -4778,6 +4946,7 @@ fn long_footnotes_continue_on_the_next_page() { #[test] fn short_footnotes_stay_whole() { + crate::test_fonts(); let mut d = Document::from_text(&"Body text line.\n".repeat(60)); let id = footnote(&mut d, &Pos::body(10, 4), "A short note, two lines long. ".repeat(4).as_str()); let l = lay(&d); @@ -4788,6 +4957,7 @@ fn short_footnotes_stay_whole() { #[test] fn footnote_longer_than_pages_ends() { + crate::test_fonts(); // A note several pages long after a one-line body: it continues on pages of its own. let mut d = Document::from_text("Body"); let id = footnote(&mut d, &Pos::body(0, 4), &"Words in a very long note. ".repeat(1500)); @@ -4830,6 +5000,7 @@ fn comment_lines(l: &DocLayout, pi: usize) -> Vec<(StoryRef, f32, f32)> { /// reply are laid out there beside the line they comment on. #[test] fn comment_balloons_widen_pages_beside_their_anchors() { + crate::test_fonts(); let mut d = Document::from_text("Intro line.\nThe quarterly figures look strong.\nClosing line."); let id = comment_on(&mut d, 1, 4, 13, "Ada Reviewer", "Check the totals", None); comment_on(&mut d, 0, 0, 0, "Ben Editor", "Totals checked", Some(id)); @@ -4856,6 +5027,7 @@ fn comment_balloons_widen_pages_beside_their_anchors() { #[test] fn no_balloons_without_anchored_comments() { + crate::test_fonts(); let d = Document::from_text("No comments here."); let l = balloon_layout(&d); assert_eq!(l.pages[0].markup, 0.0); @@ -4872,6 +5044,7 @@ fn no_balloons_without_anchored_comments() { /// one page. Nothing panics or hangs, and the cards stay on the page. #[test] fn comment_balloons_survive_hostile_comments() { + crate::test_fonts(); let mut d = Document::from_text(&"Commented words here. ".repeat(20)); let mut ids = Vec::new(); for i in 0..120 { @@ -4925,6 +5098,7 @@ fn wrapped_shape_doc(kind: wordcraft_doc::para::ShapeKind, w: f32, h: f32, float #[test] fn tight_wrap_follows_an_ellipse() { + crate::test_fonts(); use wordcraft_doc::para::{Float, ShapeKind, Wrap}; let spans = |wrap| line_spans(&lay(&wrapped_shape_doc(ShapeKind::Ellipse, 160.0, 160.0, Float { wrap, ..Default::default() }))); let (square, tight, through) = (spans(Wrap::Square), spans(Wrap::Tight), spans(Wrap::Through)); @@ -4962,6 +5136,7 @@ fn tight_wrap_follows_an_ellipse() { #[test] fn contour_wrap_turns_with_the_object() { + crate::test_fonts(); use wordcraft_doc::para::{Float, ShapeKind, Wrap}; use wordcraft_doc::wrap::WrapPolygon; // A wrap polygon covering the left half of a 200 × 100 frame. @@ -4989,6 +5164,7 @@ fn contour_wrap_turns_with_the_object() { /// symbol" a bigger shape around text at its own size. Plain `\o` overstrikes its arguments. #[test] fn enclosed_characters_draw_a_shape_around_the_text() { + crate::test_fonts(); use wordcraft_doc::eq::{EncloseShape, EncloseStyle, Enclosure}; let draw = |shape: EncloseShape, style: EncloseStyle| { let mut d = Document::from_text("a b"); @@ -5043,6 +5219,7 @@ fn enclosed_characters_draw_a_shape_around_the_text() { #[test] fn ruby_text_sits_over_its_base_and_the_line_grows_to_fit() { + crate::test_fonts(); use wordcraft_doc::para::RubyAlign; let plain = Document::from_text("Plain words here"); let mut d = Document::from_text("Plain here"); @@ -5088,6 +5265,7 @@ fn ruby_text_sits_over_its_base_and_the_line_grows_to_fit() { /// only to its right; Largest: only on its wider side. #[test] fn wrap_text_keeps_text_on_one_side() { + crate::test_fonts(); use wordcraft_doc::para::{Float, ShapeKind, Wrap, WrapText}; // A 100pt shape at `x` in the 468pt column; the lines beside it. let beside = |kind, wrap, x: f32, side| { @@ -5114,6 +5292,7 @@ fn wrap_text_keeps_text_on_one_side() { #[test] fn document_note_separators_and_continuation_notice() { + crate::test_fonts(); let mut d = Document::from_text(&"Body text line.\n".repeat(16)); let long = footnote(&mut d, &Pos::body(4, 4), &"A long footnote that runs on and on. ".repeat(100)); let sep = |cont: bool| { @@ -5169,6 +5348,7 @@ fn document_note_separators_and_continuation_notice() { /// frame, so the clip turns too); floating or inline. Hit testing follows the turned canvas. #[test] fn rotated_canvas_draws_its_clipped_members_inside_the_turn() { + crate::test_fonts(); use crate::display::Draw; use wordcraft_doc::para::{Anchor, CanvasStyle, Float, GroupChild, ShapeKind, Wrap}; let member = InlineObject::Shape { @@ -5258,6 +5438,7 @@ fn rotated_canvas_draws_its_clipped_members_inside_the_turn() { #[test] fn mirror_indents_swap_sides_on_even_pages() { + crate::test_fonts(); // #512: with mirrored indents the left indent is the inside one, on the right of an even page. let mut d = Document::from_text("odd page\neven page"); let last = Pos::body(1, 0); @@ -5318,6 +5499,7 @@ fn column_bottoms(l: &DocLayout) -> (usize, f32, usize, f32, (usize, f32)) { #[test] fn columns_before_a_continuous_break_are_balanced() { + crate::test_fonts(); use wordcraft_doc::section::SectionStart; for widths in [vec![], vec![(150.0, 36.0), (282.0, 0.0)]] { let l = lay(&two_column_section(SectionStart::Continuous, widths.clone())); @@ -5333,6 +5515,7 @@ fn columns_before_a_continuous_break_are_balanced() { #[test] fn columns_before_a_page_break_or_a_column_break_are_not_balanced() { + crate::test_fonts(); use wordcraft_doc::section::SectionStart; // Next-page break: the ten paragraphs fill the first column; the next section is on page 2. let l = lay(&two_column_section(SectionStart::NextPage, vec![])); @@ -5366,6 +5549,7 @@ fn note_marks(l: &DocLayout) -> HashMap> { #[test] fn footnotes_restart_on_each_page_and_section() { + crate::test_fonts(); use wordcraft_doc::section::{NoteProps, NoteRestart}; let mut d = Document::from_text(&"Body text line.\n".repeat(120)); let a = footnote(&mut d, &Pos::body(2, 4), "First note."); @@ -5391,6 +5575,7 @@ fn footnotes_restart_on_each_page_and_section() { #[test] fn endnotes_at_section_end_and_footnotes_below_text() { + crate::test_fonts(); use wordcraft_doc::section::{NotePos, NoteProps}; let mut d = Document::from_text("Alpha\nBeta\nGamma\nDelta"); let eid = d.add_part( @@ -5426,6 +5611,7 @@ fn endnotes_at_section_end_and_footnotes_below_text() { /// styles): Polish patterns and dictionary for Polish runs, English ones for English runs. #[test] fn hyphenation_and_proofing_follow_each_runs_language() { + crate::test_fonts(); // A tiny synthetic Polish dictionary (real ones are installed on the system, not bundled). wordcraft_proof::pl::install_dictionary("SET UTF-8\nSFX B Y 1\nSFX B y ego y\n", "2\nkonstytucyjny/B\npłynie\n").unwrap(); let text = "Konstytucyjnego żeka typography beautifull"; @@ -5460,6 +5646,7 @@ fn hyphenation_and_proofing_follow_each_runs_language() { /// or grammar" text none either; the document default language applies to text without its own. #[test] fn unproofed_languages_and_no_proof_runs_are_not_marked() { + crate::test_fonts(); // A tiny synthetic Polish dictionary (real ones are installed on the system, not bundled). wordcraft_proof::pl::install_dictionary("SET UTF-8\nSFX B Y 1\nSFX B y ego y\n", "2\nkonstytucyjny/B\npłynie\n").unwrap(); let opts = LayoutOptions { proofing: true, ..LayoutOptions::default() }; @@ -5484,6 +5671,7 @@ fn unproofed_languages_and_no_proof_runs_are_not_marked() { /// ignored), on every page, like the same fields in the footer. #[test] fn body_page_totals_count_every_page() { + crate::test_fonts(); let mut d = Document::from_text("Body-1\nBody-2\nBody-3"); let field = |instr: &str| InlineObject::Field { instr: instr.into(), result: "9".into(), locked: false, dirty: false, code: false, form: String::new() }; @@ -5522,6 +5710,7 @@ fn body_page_totals_count_every_page() { /// a body paragraph. #[test] fn table_cell_footnotes_are_placed() { + crate::test_fonts(); let mut d = Document::from_text("After the table."); d.insert_block(StoryRef::Body, &Path::top(0), wordcraft_doc::Block::Table(wordcraft_doc::Table::new(1, 1, 300.0))).unwrap(); let cell = Pos { story: StoryRef::Body, path: Path(vec![0, 0, 0, 0]), off: 0 }; @@ -5537,6 +5726,7 @@ fn table_cell_footnotes_are_placed() { #[test] fn east_asian_text_takes_the_asian_font_and_latin_text_the_latin_font() { + crate::test_fonts(); // #505: one run with a Latin font and an Asian font (w:eastAsia) of its own sets each script in // its font. Prefer an installed CJK font other than the one fallback would pick anyway. let resolve = |f: &str| wordcraft_fonts::word::resolve(f, false, false); @@ -5578,6 +5768,7 @@ fn east_asian_text_takes_the_asian_font_and_latin_text_the_latin_font() { /// the body lay out a second time; one in the body still does. #[test] fn footer_page_total_needs_one_body_pass() { + crate::test_fonts(); let field = || InlineObject::Field { instr: "NUMPAGES".into(), result: "9".into(), locked: false, dirty: false, code: false, form: String::new() }; let mut d = Document::from_text("Body-1\nBody-2\nBody-3"); @@ -5604,6 +5795,7 @@ fn footer_page_total_needs_one_body_pass() { /// Cyrillic text in an English run is not hyphenated (the English data has nothing to say about it). #[test] fn bulgarian_text_is_hyphenated_by_its_own_rules() { + crate::test_fonts(); let text = "Сричкопренасянето в професионалните документи задължително следва правописните правила, разработени от специалистите. ".repeat(6); let hyphenated = |d: &Document| -> Vec<(String, String)> { let mut out = Vec::new(); @@ -5641,6 +5833,7 @@ fn bulgarian_text_is_hyphenated_by_its_own_rules() { /// grow it). Tables of contents with big tab runs were a third taller than Word's. #[test] fn spaces_and_tabs_do_not_make_a_line_taller() { + crate::test_fonts(); use wordcraft_doc::props::CharProps; let small = CharProps { size: Some(9.0), ..Default::default() }; let big = CharProps { size: Some(20.0), ..Default::default() }; @@ -5676,6 +5869,7 @@ fn spaces_and_tabs_do_not_make_a_line_taller() { /// the page bottom. #[test] fn a_list_label_adds_ascent_but_not_descent() { + crate::test_fonts(); let first_line = |label_size: Option| { let mut d = Document::new(); let num = d.numbering.add_list(wordcraft_doc::ListKind::Numbered); @@ -5706,6 +5900,7 @@ fn a_list_label_adds_ascent_but_not_descent() { /// with text is never hidden. NIH's Table A showed a blank row Word doesn't. #[test] fn a_hidden_cell_mark_takes_no_room() { + crate::test_fonts(); let row3_top = |hide: [bool; 2], second: Option<&str>| { let mut d = Document::from_text("Above"); let mut t = Table::new(3, 2, 300.0); @@ -5760,6 +5955,7 @@ fn a_hidden_cell_mark_takes_no_room() { /// an ordinary paragraph, a line of its own above the footer text. #[test] fn a_text_frame_in_a_cell_floats_beside_the_text() { + crate::test_fonts(); use wordcraft_doc::props::TextFrame; let mut d = Document::from_text("Above"); let mut t = Table::new(1, 1, 300.0); @@ -5789,6 +5985,7 @@ fn a_text_frame_in_a_cell_floats_beside_the_text() { /// the list's 0.6", and its "sample text" paragraphs with the list off at 0.9" instead of the margin. #[test] fn list_indents_and_style_indents_combine_like_word() { + crate::test_fonts(); use wordcraft_doc::props::NumRef; use wordcraft_doc::styles::{Style, StyleKind}; let text_x = |style: &str, own: ParaProps| { @@ -5840,6 +6037,7 @@ fn list_indents_and_style_indents_combine_like_word() { /// five-line kept paragraph left a third of a page empty. #[test] fn keep_with_next_moves_a_paragraph_only_when_it_would_fit() { + crate::test_fonts(); // Page of each of the kept paragraph's 5 lines and of the first paragraph after it. let pages = |fillers: usize, kept: ParaProps, next: ParaProps| { let mut d = Document::new(); @@ -5886,6 +6084,7 @@ fn keep_with_next_moves_a_paragraph_only_when_it_would_fit() { /// and the heading keeps it company. #[test] fn keep_with_next_carries_the_heading_when_widow_control_moves_the_next_paragraph() { + crate::test_fonts(); // Page of the heading, then of the next paragraph's lines at "Bb", "Bc" and "Be". let pages = |fillers: usize, widow: bool| -> [usize; 4] { let mut d = Document::new(); @@ -5930,6 +6129,7 @@ fn keep_with_next_carries_the_heading_when_widow_control_moves_the_next_paragrap /// over when the last one cannot start on the page, and all stay when it can. #[test] fn keep_with_next_chain_carries_over_when_the_last_paragraph_cannot_start() { + crate::test_fonts(); let pages = |fillers: usize, widow: bool| -> Vec { let mut d = Document::new(); let plain = ParaProps { @@ -5972,6 +6172,7 @@ fn keep_with_next_chain_carries_over_when_the_last_paragraph_cannot_start() { /// of a paragraph that keep lines together will not split (#966). #[test] fn keep_with_next_carries_the_heading_when_keep_lines_moves_the_next_paragraph() { + crate::test_fonts(); let pages = |fillers: usize, kept_lines: bool| -> Vec { let mut d = Document::new(); let plain = ParaProps { @@ -6016,6 +6217,7 @@ fn keep_with_next_carries_the_heading_when_keep_lines_moves_the_next_paragraph() /// many continuous breaks. #[test] fn an_empty_continuous_section_mark_takes_no_room() { + crate::test_fonts(); use wordcraft_doc::section::{SectionProps, SectionStart}; // "Pa", the section's last paragraph (`mark`), "Pb", "Pc" in the next section of `cols` // columns, then "Pd" after a second break. @@ -6053,6 +6255,7 @@ fn an_empty_continuous_section_mark_takes_no_room() { /// form's field boxes sat a space-before too low, over their labels. #[test] fn paragraph_relative_floats_start_above_the_space_before() { + crate::test_fonts(); let shape_top = |before: f32, prev_after: f32, y: f32| { let float = wordcraft_doc::para::Float { wrap: wordcraft_doc::para::Wrap::InFrontOfText, @@ -6107,6 +6310,7 @@ fn paragraph_relative_floats_start_above_the_space_before() { /// column it started in, so a paragraph took a dozen lines a word or two wide. #[test] fn text_after_a_column_break_takes_the_new_column_s_width() { + crate::test_fonts(); let mut d = Document::new(); d.last_section.columns = wordcraft_doc::section::Columns { count: 2, space: 36.0, separator: false, widths: vec![(67.0, 52.0), (348.0, 0.0)] }; let brk = wordcraft_doc::para::COLUMN_BREAK; @@ -6127,6 +6331,7 @@ fn text_after_a_column_break_takes_the_new_column_s_width() { /// modes up to 14 (less the previous paragraph's space after) and drops it in mode 15. #[test] fn space_before_at_the_top_of_a_page_like_word() { + crate::test_fonts(); let para = |text: &str, before: f32, pbb: bool| { let mut p = wordcraft_doc::Paragraph::with_text(text, Default::default()); p.props = ParaProps { space_before: Some(before), space_after: Some(0.0), page_break_before: Some(pbb), ..Default::default() }; @@ -6209,6 +6414,7 @@ fn space_before_at_the_top_of_a_page_like_word() { /// all unkerned). #[test] fn kerning_follows_kerning_for_fonts_like_word() { + crate::test_fonts(); use wordcraft_doc::props::CharProps; let width = |kern: Option| { let mut d = Document::new(); @@ -6234,6 +6440,7 @@ fn kerning_follows_kerning_for_fonts_like_word() { /// cell; next to another paragraph's space the larger of the two counts. #[test] fn automatic_paragraph_spacing_like_word() { + crate::test_fonts(); let auto = |before: bool, after: bool| ParaProps { space_before: Some(5.0), space_after: Some(5.0), @@ -6281,6 +6488,7 @@ fn automatic_paragraph_spacing_like_word() { /// In a table cell automatic spacing drops at the cell's top and bottom (Word for Windows). #[test] fn automatic_paragraph_spacing_drops_at_cell_edges() { + crate::test_fonts(); let cell_tops = |auto: bool| { let mut d = Document::from_text("Above"); let mut t = Table::new(1, 1, 300.0); @@ -6319,6 +6527,7 @@ fn automatic_paragraph_spacing_drops_at_cell_edges() { /// (each item automatic before and after) were 14 pt apart instead of touching. #[test] fn automatic_spacing_drops_between_list_items() { + crate::test_fonts(); use wordcraft_doc::props::NumRef; let auto = ParaProps { space_before: Some(5.0), @@ -6395,6 +6604,7 @@ fn automatic_spacing_drops_between_list_items() { /// `hyphenation_points` keeps it so whatever the patterns become.) #[test] fn automatic_hyphenation_leaves_vietnamese_whole() { + crate::test_fonts(); let text = "Những người khuyến khích chuyện trường thường nghiêng nghiêng nhưng thành phố Nguyễn Trường Thượng xuyên suốt chương trình. ".repeat(8); let mut d = Document::from_text(&text); @@ -6411,6 +6621,7 @@ fn automatic_hyphenation_leaves_vietnamese_whole() { /// them), and so is the text drawn on them; a paragraph's shading is an ordinary fill. #[test] fn highlight_fill_is_marked_as_a_text_background() { + crate::test_fonts(); use wordcraft_doc::props::Highlight; let cp = wordcraft_doc::CharProps { highlight: Some(Highlight::Yellow), ..Default::default() }; let mut p = wordcraft_doc::Paragraph::with_text("Marked", cp); @@ -6437,6 +6648,7 @@ fn highlight_fill_is_marked_as_a_text_background() { /// its glyphs; other lines (here a paragraph border) don't. #[test] fn underline_on_a_highlight_carries_the_text_background() { + crate::test_fonts(); use wordcraft_doc::props::{Highlight, Underline}; let cp = wordcraft_doc::CharProps { highlight: Some(Highlight::Teal), underline: Some(Underline::Single), strike: Some(true), ..Default::default() }; @@ -6511,6 +6723,7 @@ fn assert_contiguous(ranges: &[std::ops::Range], len: usize) { /// #1352: a PAGE field after a page break inside its paragraph shows the page it is on. #[test] fn page_field_after_intra_paragraph_break_shows_its_page() { + crate::test_fonts(); let brk = wordcraft_doc::para::PAGE_BREAK; let d = page_field_doc(0, &["A", &format!("{brk}B")]); let l = lay(&d); @@ -6522,6 +6735,7 @@ fn page_field_after_intra_paragraph_break_shows_its_page() { /// #1352: a paragraph running over onto the next page shows that page's number in its tail. #[test] fn page_field_in_a_split_paragraph_tail_shows_its_page() { + crate::test_fonts(); let long = "words that fill the page ".repeat(400); let d = page_field_doc(0, &["Head", &long]); let l = lay(&d); @@ -6537,6 +6751,7 @@ fn page_field_in_a_split_paragraph_tail_shows_its_page() { /// again for the wider results, and the paragraph's text is still placed exactly once. #[test] fn page_fields_from_page_nine_to_ten_rebreak_the_rest() { + crate::test_fonts(); let pieces: Vec = (0..900).map(|i| format!(" word{i}")).collect(); let pieces: Vec<&str> = pieces.iter().map(String::as_str).collect(); let d = page_field_doc(8, &pieces); @@ -6569,6 +6784,7 @@ fn page_fields_from_page_nine_to_ten_rebreak_the_rest() { /// shows the page the footnote is on (the paragraph's own page map doesn't leak into it). #[test] fn footnote_page_field_from_a_split_paragraph_shows_its_page() { + crate::test_fonts(); let long = "words that fill the page ".repeat(200); let mut d = page_field_doc(0, &["Head", &long, "Tail"]); let end = d.end_of(StoryRef::Body); @@ -6644,6 +6860,7 @@ fn equation_numbers(l: &DocLayout) -> Vec { /// for a later page's PAGE fields) doesn't count its numbered equations twice: they read (1), (2), (3). #[test] fn relaid_paragraphs_keep_their_equation_numbers() { + crate::test_fonts(); let eq = |s: &str| InlineObject::Equation { linear: s.into(), display: true, @@ -6714,6 +6931,7 @@ fn para_pieces(l: &DocLayout, t: usize) -> (Vec<(usize, usize, usize, f32)>, usi #[test] fn space_after_stays_clear_of_the_footnote_separator() { + crate::test_fonts(); // #1116, measured in Word: on a page with footnotes a paragraph ending there keeps its space // after above the separator, or its last lines (two, with widow control) go to the next page // with the note. @@ -6742,6 +6960,7 @@ fn space_after_stays_clear_of_the_footnote_separator() { #[test] fn space_after_may_overflow_the_bottom_margin_without_notes() { + crate::test_fonts(); // Without footnotes nothing changes: the last line only has to fit, its space after may run // past the bottom margin (Word). let mut tight = 0; @@ -6784,6 +7003,7 @@ fn para_props(d: &mut Document, i: usize) -> &mut ParaProps { #[test] fn space_after_stays_clear_when_only_the_next_paragraph_has_contextual_spacing() { + crate::test_fonts(); // Contextual spacing on the next paragraph alone drops its space before, not this one's space // after (as the spacing between them): the space after still stays clear of the separator. space_after_clear_with(|d, t| para_props(d, t + 1).contextual_spacing = Some(true), 6.0); @@ -6791,6 +7011,7 @@ fn space_after_stays_clear_when_only_the_next_paragraph_has_contextual_spacing() #[test] fn space_after_stays_clear_before_a_numbered_paragraph_without_a_label() { + crate::test_fonts(); // Automatic spacing drops only between two list items with labels: before a paragraph whose // list doesn't exist (no label) a list item's automatic space after stays clear. use wordcraft_doc::props::NumRef; @@ -6808,6 +7029,7 @@ fn space_after_stays_clear_before_a_numbered_paragraph_without_a_label() { #[test] fn cell_widths_override_grid() { + crate::test_fonts(); for fixed in [false, true] { let mut t = Table::new(2, 3, 450.0); t.props.fixed = fixed; @@ -6831,6 +7053,7 @@ fn cell_widths_override_grid() { #[test] fn ambiguous_widths_keep_grid() { + crate::test_fonts(); let mut t = Table::new(2, 3, 450.0); t.grid = vec![40.0, 175.0, 235.0]; for row in &mut t.rows { @@ -6925,6 +7148,7 @@ fn assert_clear_of(lines: &[(f32, f32, f32, f32)], r: Rect, what: &str) { /// #1374: a paragraph that runs on from column 1 into column 2 wraps around a shape in column 2. #[test] fn paragraph_continued_into_next_column_wraps_around_a_shape_there() { + crate::test_fonts(); let r = Rect::new(340.0, 300.0, 100.0, 100.0); let d = column_wrap_doc(2, Some(page_shape(r.x, r.y, r.w, r.h)), 450); let l = lay(&d); @@ -6939,6 +7163,7 @@ fn paragraph_continued_into_next_column_wraps_around_a_shape_there() { /// from it) doesn't make the paragraph going into that column be laid out again. #[test] fn paragraph_continued_into_next_column_ignores_shapes_well_clear_of_it() { + crate::test_fonts(); let d = column_wrap_doc(2, Some(page_shape(0.0, 300.0, 20.0, 100.0)), 450); WRAP_RELAYS.with(|c| c.set(0)); let l = lay(&d); @@ -6951,6 +7176,7 @@ fn paragraph_continued_into_next_column_ignores_shapes_well_clear_of_it() { /// (reaching below its body) doesn't push them aside there. #[test] fn paragraph_continued_onto_next_page_is_broken_for_that_page() { + crate::test_fonts(); // On page 1 from 640 down past the page bottom, at the left of the text. let d = column_wrap_doc(1, Some(page_shape(72.0, 640.0, 150.0, 150.0)), 900); let l = lay(&d); @@ -6971,6 +7197,7 @@ fn paragraph_continued_onto_next_page_is_broken_for_that_page() { /// the first: the paragraph isn't laid out again (no wrap relay), and both pieces share one layout. #[test] fn paragraph_continued_into_next_column_without_shapes_is_not_laid_out_again() { + crate::test_fonts(); let d = column_wrap_doc(2, None, 450); WRAP_RELAYS.with(|c| c.set(0)); let l = lay(&d); @@ -6991,6 +7218,7 @@ fn paragraph_continued_into_next_column_without_shapes_is_not_laid_out_again() { /// when its lines in column 2 are broken again around a shape there. #[test] fn paragraph_own_float_is_not_replaced_when_its_rest_wraps_in_the_next_column() { + crate::test_fonts(); use wordcraft_doc::para::{Anchor, Float, ShapeKind, Wrap}; let r = Rect::new(340.0, 300.0, 100.0, 100.0); let mut d = column_wrap_doc(2, Some(page_shape(r.x, r.y, r.w, r.h)), 450); @@ -7057,6 +7285,7 @@ fn text_clusters(pl: &ParaLayout) -> Vec<(usize, usize, Vec, f32, f32)> { /// caret, selection and hit testing (#1581). #[test] fn caps_draw_sharp_s_as_two_capitals_in_one_cluster() { + crate::test_fonts(); let width = |pl: &ParaLayout| text_clusters(pl).iter().map(|c| c.4).sum::(); let plain_strase = { let d = Document::from_text("STRASE"); @@ -7104,6 +7333,7 @@ fn caps_draw_sharp_s_as_two_capitals_in_one_cluster() { /// A caps run in a symbol font draws each code as its symbol: ß never becomes two glyphs (#1581). #[test] fn caps_in_a_symbol_font_never_expand_characters() { + crate::test_fonts(); let pl = strasse_with(&|c| { c.caps = Some(true); c.font = Some("Wingdings".into()); @@ -7121,6 +7351,7 @@ fn caps_in_a_symbol_font_never_expand_characters() { /// is the page's last block. A page with one block keeps it at the top. #[test] fn section_vertical_alignment_top_center_justify_bottom() { + crate::test_fonts(); use wordcraft_doc::section::PageVAlign; let para = |text: &str| { let mut p = wordcraft_doc::Paragraph::with_text(text, Default::default()); @@ -7170,6 +7401,7 @@ fn section_vertical_alignment_top_center_justify_bottom() { /// section's (Justified then Top: spread; Top then Justified: stays at the top). #[test] fn a_sections_last_page_keeps_its_own_vertical_alignment() { + crate::test_fonts(); use wordcraft_doc::section::{PageVAlign, SectionProps}; let para = |text: &str| { let mut p = wordcraft_doc::Paragraph::with_text(text, Default::default()); @@ -7197,6 +7429,7 @@ fn a_sections_last_page_keeps_its_own_vertical_alignment() { /// after inside the text area, so "Three" with 72 pt after sits 72 pt above the bottom margin. #[test] fn justified_page_keeps_the_last_paragraphs_space_after() { + crate::test_fonts(); let mut d = Document::new(); d.body = ["One", "Two", "Three"] .iter() @@ -7219,6 +7452,7 @@ fn justified_page_keeps_the_last_paragraphs_space_after() { /// #1320: a justified page with several newspaper columns stays at the top (not spread). #[test] fn justified_multi_column_pages_stay_at_the_top() { + crate::test_fonts(); let laid = |va| { let mut d = two_column_section(wordcraft_doc::section::SectionStart::NextPage, Vec::new()); d.para_mut(StoryRef::Body, &Path::top(9)).unwrap().section.as_mut().unwrap().valign = va; @@ -7238,6 +7472,7 @@ fn justified_multi_column_pages_stay_at_the_top() { #[test] fn row_borders_stay_on_row() { + crate::test_fonts(); let mut d = Document::from_text("before\nafter"); let mut t = Table::new(2, 2, 200.0); t.props.style = None; @@ -7253,6 +7488,7 @@ fn row_borders_stay_on_row() { #[test] fn row_nil_overrides_style() { + crate::test_fonts(); let mut d = Document::from_text("before\nafter"); let mut t = Table::new(1, 1, 200.0); t.rows[0].props.exceptions = Some(Box::new(wordcraft_doc::props::TableProps { diff --git a/crates/layout/src/tests_bidi.rs b/crates/layout/src/tests_bidi.rs index 5d20141f6f..0d0a2602d5 100644 --- a/crates/layout/src/tests_bidi.rs +++ b/crates/layout/src/tests_bidi.rs @@ -58,6 +58,7 @@ fn at(s: &str, n: usize) -> usize { #[test] fn visual_order_follows_uax9_rule_l2() { + crate::test_fonts(); // The example from UAX #9 / unicode-bidi: levels 0 0 0 1 1 1 2 2. assert_eq!(visual_order(&[0, 0, 0, 1, 1, 1, 2, 2]), vec![0, 1, 2, 6, 7, 5, 4, 3]); assert_eq!(visual_order(&[1, 1, 1]), vec![2, 1, 0]); @@ -69,6 +70,7 @@ fn visual_order_follows_uax9_rule_l2() { #[test] fn levels_for_persian_with_latin_and_numbers() { + crate::test_fonts(); assert!(bidi_levels("plain English", false).is_empty(), "pure LTR paragraphs skip the algorithm"); let s = "سلام Word ۱۴۰۳"; let lv = bidi_levels(s, true); @@ -84,6 +86,7 @@ fn levels_for_persian_with_latin_and_numbers() { #[test] fn rtl_paragraph_is_right_aligned_and_reads_right_to_left() { + crate::test_fonts(); let s = "سلام دنیا"; let d = doc_of(&[(s, true)]); let l = lay(&d); @@ -100,6 +103,7 @@ fn rtl_paragraph_is_right_aligned_and_reads_right_to_left() { #[test] fn ltr_paragraph_with_embedded_persian_word() { + crate::test_fonts(); let s = "Hello سلام world"; let d = doc_of(&[(s, false)]); let l = lay(&d); @@ -110,6 +114,7 @@ fn ltr_paragraph_with_embedded_persian_word() { #[test] fn numbers_and_latin_inside_persian_keep_their_order() { + crate::test_fonts(); // "Price 250 toman" in Persian, with a Latin product name and Persian punctuation. let s = "قیمت 250 تومان برای Word، امسال ۱۴۰۳."; let d = doc_of(&[(s, true)]); @@ -131,6 +136,7 @@ fn numbers_and_latin_inside_persian_keep_their_order() { #[test] fn caret_and_hit_agree_in_bidi_text() { + crate::test_fonts(); for (s, rtl) in [("سلام world ۱۲۳", true), ("Hello سلام دنیا end", false), ("کتاب «WordCraft» (نسخه ۲)", true)] { let d = doc_of(&[(s, rtl)]); let l = lay(&d); @@ -151,6 +157,7 @@ fn caret_and_hit_agree_in_bidi_text() { #[test] fn visual_steps_cover_the_line_in_order() { + crate::test_fonts(); let s = "سلام world"; let d = doc_of(&[(s, true)]); let l = lay(&d); @@ -181,6 +188,7 @@ fn visual_steps_cover_the_line_in_order() { #[test] fn selection_of_mixed_text_can_be_two_pieces() { + crate::test_fonts(); // Logical range "lo wo" of "Hello سلام world"? Select across the Persian word's start: // "lo سل" is two visual pieces (the end of Hello, and the right part of سلام). let s = "Hello سلام world"; @@ -195,6 +203,7 @@ fn selection_of_mixed_text_can_be_two_pieces() { #[test] fn rtl_indents_and_alignment_are_measured_from_the_right() { + crate::test_fonts(); let s = "متن فارسی"; let mut d = doc_of(&[(s, true), (s, true), (s, true)]); let p0 = Pos::body(0, 0); @@ -213,6 +222,7 @@ fn rtl_indents_and_alignment_are_measured_from_the_right() { #[test] fn rtl_list_label_sits_on_the_right() { + crate::test_fonts(); let mut d = doc_of(&[("مورد اول", true), ("مورد دوم", true)]); let id = d.numbering.add_list(wordcraft_doc::ListKind::Numbered); d.format_paragraphs(&Pos::body(0, 0), &Pos::body(1, 0), &|p: &mut ParaProps| { @@ -234,6 +244,7 @@ fn rtl_list_label_sits_on_the_right() { #[test] fn rtl_empty_and_trailing_lines_mirror() { + crate::test_fonts(); let d = doc_of(&[("", true)]); let l = lay(&d); assert!((caret_x(&l, 0, 0) - 540.0).abs() < 0.5, "empty right-to-left paragraph: caret at the right margin"); @@ -247,6 +258,7 @@ fn rtl_empty_and_trailing_lines_mirror() { #[test] fn bidi_display_draws_every_glyph_inside_the_line() { + crate::test_fonts(); let s = "این یک متن آزمایشی با English و عدد ۱۲۳ است. ".repeat(12); let d = doc_of(&[(&s, true)]); let l = lay(&d); @@ -273,6 +285,7 @@ fn bidi_display_draws_every_glyph_inside_the_line() { #[test] fn hostile_bidi_text_never_panics() { + crate::test_fonts(); // Explicit embeddings, isolates, unmatched pops, marks with no base, lone surrogates' stand-ins. let junk = [ "\u{202B}\u{202B}\u{202C}\u{202C}\u{202C}abc\u{2067}سلام\u{2069}\u{2069}", @@ -318,6 +331,7 @@ fn cluster_glyphs(glyphs: &[wordcraft_fonts::ShapedGlyph]) -> Vec> { #[test] fn persian_letters_join_and_brackets_mirror() { + crate::test_fonts(); let Some(face) = arabic_face() else { eprintln!("skipped: no font with Persian letters installed"); return; @@ -347,6 +361,7 @@ fn persian_letters_join_and_brackets_mirror() { #[test] fn persian_text_in_a_latin_font_stays_in_one_fallback_face() { + crate::test_fonts(); if arabic_face().is_none() { eprintln!("skipped: no font with Persian letters installed"); return; @@ -365,6 +380,7 @@ fn persian_text_in_a_latin_font_stays_in_one_fallback_face() { #[test] fn glyph_runs_carry_their_text_for_pdf_export() { + crate::test_fonts(); let s = "سلام WordCraft می‌خواهم"; let d = doc_of(&[(s, true)]); let l = lay(&d); @@ -394,6 +410,7 @@ fn glyph_runs_carry_their_text_for_pdf_export() { #[test] fn character_border_boxes_a_whole_rtl_word() { + crate::test_fonts(); let s = "کتاب خوب"; let mut d = doc_of(&[(s, true)]); let b = wordcraft_doc::props::Border { space: 0.0, ..wordcraft_doc::props::Border::single(0.5) }; @@ -418,6 +435,7 @@ fn character_border_boxes_a_whole_rtl_word() { #[test] fn inline_picture_in_rtl_text_sits_in_its_place() { + crate::test_fonts(); // "متن [picture] بیشتر": in a right-to-left paragraph the picture is drawn where its cluster // is on the line (left of the first word), not at its logical x. let s = "متن بیشتر"; @@ -455,6 +473,7 @@ fn inline_picture_in_rtl_text_sits_in_its_place() { /// margin and border are drawn on its right. Clicks find cells by where they are drawn. #[test] fn rtl_table_lays_its_columns_out_right_to_left() { + crate::test_fonts(); use wordcraft_doc::props::{Border, Borders}; use wordcraft_doc::{Path, Table}; let mut d = Document::from_text("before\nafter"); @@ -502,6 +521,7 @@ fn rtl_table_lays_its_columns_out_right_to_left() { /// first column at the right, and the line between them stays midway. #[test] fn rtl_section_puts_its_first_column_on_the_right() { + crate::test_fonts(); use wordcraft_doc::section::Columns; let text = "Columns flow from the right edge in a right-to-left section. ".repeat(150); let mut d = Document::from_text(&text); @@ -522,6 +542,7 @@ fn rtl_section_puts_its_first_column_on_the_right() { #[test] fn list_label_moves_with_its_text_when_the_paragraph_is_aligned() { + crate::test_fonts(); // Issue #1159: centring or end-aligning a list item moved the text and left the bullet at the // margin. In Word the label stays attached to the start of the text, in both directions. for (rtl, text) in [(false, "alpha beta"), (true, "مورد اول")] { diff --git a/crates/layout/src/tests_complex.rs b/crates/layout/src/tests_complex.rs index d8e6b693f0..e61c11246e 100644 --- a/crates/layout/src/tests_complex.rs +++ b/crates/layout/src/tests_complex.rs @@ -29,6 +29,7 @@ const MYANMAR: &str = "\u{1019}\u{103C}\u{1014}\u{103A}\u{1019}\u{102C}\u{1005}\ #[test] fn myanmar_syllables_are_single_clusters() { + crate::test_fonts(); let l = lay(&Document::from_text(MYANMAR)); let pl = para_layout(&l, 0); let starts: Vec = pl.clusters.iter().map(|c| c.start).collect(); @@ -46,6 +47,7 @@ fn myanmar_syllables_are_single_clusters() { #[test] fn myanmar_without_spaces_wraps_between_syllables() { + crate::test_fonts(); let text = MYANMAR.repeat(80); let l = lay(&Document::from_text(&text)); let pl = para_layout(&l, 0); @@ -71,6 +73,7 @@ fn myanmar_face() -> Option { #[test] fn myanmar_prevowel_is_drawn_before_its_consonant() { + crate::test_fonts(); let Some(face) = myanmar_face() else { eprintln!("skipped: no Myanmar font with OpenType tables installed"); return; @@ -92,6 +95,7 @@ fn myanmar_prevowel_is_drawn_before_its_consonant() { #[test] fn myanmar_in_a_latin_font_falls_back_to_a_regular_myanmar_face() { + crate::test_fonts(); if myanmar_face().is_none() { eprintln!("skipped: no Myanmar font with OpenType tables installed"); return; diff --git a/crates/layout/src/tests_grid.rs b/crates/layout/src/tests_grid.rs index fc2c9005a7..a71a78bb90 100644 --- a/crates/layout/src/tests_grid.rs +++ b/crates/layout/src/tests_grid.rs @@ -41,6 +41,7 @@ fn lines(l: &DocLayout) -> (usize, Vec) { #[test] fn line_grid_snaps_line_heights_and_fixes_lines_per_page() { + crate::test_fonts(); let text = vec!["Grid line"; 80].join("\n"); let d = with_grid(&text, DocGridType::Lines, 0); let (first, hs) = lines(&lay(&d)); @@ -64,6 +65,7 @@ fn line_grid_snaps_line_heights_and_fixes_lines_per_page() { #[test] fn character_grid_places_east_asian_text_on_the_pitch() { + crate::test_fonts(); let text = "日本語の文書abc"; // The default font size plus 2.5 pt: a pitch unlike any font's advance. let lines = wordcraft_doc::SectionProps { doc_grid: Some(DocGrid { kind: DocGridType::Lines, ..Default::default() }), ..Default::default() }; diff --git a/crates/layout/src/tests_numeral.rs b/crates/layout/src/tests_numeral.rs index 9de3e8ee8f..542d14516c 100644 --- a/crates/layout/src/tests_numeral.rs +++ b/crates/layout/src/tests_numeral.rs @@ -37,6 +37,7 @@ fn line_gids(l: &DocLayout, text: &str) -> Vec> { #[test] fn parse_accepts_the_four_modes_any_case() { + crate::test_fonts(); assert_eq!(NumeralMode::parse("arabic"), Some(NumeralMode::Arabic)); assert_eq!(NumeralMode::parse("hindi"), Some(NumeralMode::Hindi)); assert_eq!(NumeralMode::parse("context"), Some(NumeralMode::Context)); @@ -50,6 +51,7 @@ fn parse_accepts_the_four_modes_any_case() { #[test] fn map_substitutes_digits_both_ways() { + crate::test_fonts(); for d in '0'..='9' { let hindi = char::from_u32(0x660 + (d as u32 - '0' as u32)).unwrap(); assert_eq!(NumeralMode::Hindi.map(d, false), hindi); @@ -73,6 +75,7 @@ fn map_substitutes_digits_both_ways() { #[test] fn resolve_uses_the_given_system_locale() { + crate::test_fonts(); for m in [NumeralMode::Arabic, NumeralMode::Hindi, NumeralMode::Context] { assert_eq!(m.resolve(Some("ar-EG")), m, "only System is resolved"); } @@ -87,6 +90,7 @@ fn resolve_uses_the_given_system_locale() { #[test] fn context_mode_on_a_very_long_digit_run_stays_linear() { + crate::test_fonts(); // Every digit used to scan back over all the digits and spaces before it (O(n²)). let text = format!("سنة {}", "1 ".repeat(100_000)); let start = std::time::Instant::now(); @@ -100,6 +104,7 @@ fn context_mode_on_a_very_long_digit_run_stays_linear() { #[test] fn hindi_mode_reshapes_digits_but_keeps_offsets() { + crate::test_fonts(); let text = "Year 2024!"; let latin = lay_opts(text, false, NumeralMode::Arabic); let hindi = lay_opts(text, false, NumeralMode::Hindi); @@ -121,6 +126,7 @@ fn hindi_mode_reshapes_digits_but_keeps_offsets() { #[test] fn arabic_mode_converts_hindi_digits_back() { + crate::test_fonts(); let text = "عام ٢٠٢٤"; let hindi = lay_opts(text, true, NumeralMode::Hindi); let arabic = lay_opts(text, true, NumeralMode::Arabic); @@ -139,6 +145,7 @@ fn arabic_mode_converts_hindi_digits_back() { #[test] fn context_mode_follows_run_direction() { + crate::test_fonts(); // Right-to-left paragraph: digits behave like Hindi mode. let rtl = "سنة 2024"; let ctx = line_gids(&lay_opts(rtl, true, NumeralMode::Context), rtl); @@ -158,6 +165,7 @@ fn context_mode_follows_run_direction() { #[test] fn remap_range_maps_display_bytes_to_original() { + crate::test_fonts(); use crate::para::remap_range; // "a5ب": '5' (1 byte) displays as '٥' (2 bytes), so display offsets shift after it. let orig = "a5ب"; diff --git a/crates/layout/src/tests_pages.rs b/crates/layout/src/tests_pages.rs index c19252cb5f..6e08c1328b 100644 --- a/crates/layout/src/tests_pages.rs +++ b/crates/layout/src/tests_pages.rs @@ -35,6 +35,7 @@ fn body_left(l: &DocLayout) -> Vec { #[test] fn mirrored_margins_put_the_inside_margin_toward_the_binding() { + crate::test_fonts(); // #452: inside 108 (plus a 10pt gutter), outside 36. Odd pages: inside on the left; even // pages: on the right. let mut d = Document::from_text(&"Body line\n".repeat(90)); @@ -55,6 +56,7 @@ fn mirrored_margins_put_the_inside_margin_toward_the_binding() { #[test] fn inside_and_outside_floats_swap_sides_on_even_pages() { + crate::test_fonts(); // #1181: shapes Word places in the outside margin of a book (mirrored margins) all came out // in the right margin. Inside 108 plus a 10pt gutter, outside 36; the paragraph on page 2 // carries the same six shapes as page 1's, told apart by their widths. @@ -124,6 +126,7 @@ fn inside_and_outside_floats_swap_sides_on_even_pages() { #[test] fn outside_floating_tables_swap_sides_on_even_pages() { + crate::test_fonts(); // #1181, for a floating table (`w:tblpXSpec="outside"`): on page 2 of a book the outside is // the left. One positioned from the page edge stays there on both pages. use wordcraft_doc::para::{Anchor, FloatAlign}; @@ -159,6 +162,7 @@ fn outside_floating_tables_swap_sides_on_even_pages() { #[test] fn a_tall_footer_pushes_the_body_up() { + crate::test_fonts(); // #453: an eight-line footer reaches far above the 72pt bottom margin. let mut d = Document::from_text(&"Body line\n".repeat(60)); let id = d.add_part(PartKind::Footer, paras("Footer line", 8)); @@ -184,6 +188,7 @@ fn a_tall_footer_pushes_the_body_up() { #[test] fn a_linked_tall_header_pushes_the_next_sections_body_down() { + crate::test_fonts(); // #454: section 2 has no header of its own and shows section 1's (link to previous). let mut d = Document::from_text("First section\nSecond section"); let id = d.add_part(PartKind::Header, paras("Header line", 8)); @@ -203,6 +208,7 @@ fn a_linked_tall_header_pushes_the_next_sections_body_down() { #[test] fn long_endnotes_continue_on_new_pages() { + crate::test_fonts(); // #456: an 80-paragraph endnote after a one-line body. let mut d = Document::from_text("Body"); let id = d.add_part(PartKind::Endnote, paras("Endnote paragraph", 80)); @@ -230,6 +236,7 @@ fn long_endnotes_continue_on_new_pages() { #[test] fn caret_in_a_continued_footnote_is_found_on_its_page() { + crate::test_fonts(); // #457: one long footnote paragraph split over pages 1 and 2. let mut d = Document::from_text(&"Body text line.\n".repeat(16)); let id = d.add_part( @@ -264,6 +271,7 @@ fn caret_in_a_continued_footnote_is_found_on_its_page() { /// own pages). The footer shows each letter's last page, not the file's stale result. #[test] fn page_references_in_footers_show_their_bookmarks_page() { + crate::test_fonts(); let field = |instr: &str| InlineObject::Field { instr: instr.into(), result: "9".into(), locked: false, dirty: false, code: false, form: String::new() }; let footer = |d: &mut Document, mark: &str| { @@ -379,6 +387,7 @@ fn mark(d: &mut Document, path: &Path, off: usize, name: &str) { /// section numbered in roman numerals are found on the page and in the format they land in. #[test] fn page_references_find_bookmarks_in_split_paragraphs_tables_and_roman_sections() { + crate::test_fonts(); // One long paragraph over two pages, marked at its start and its end. let mut d = Document::from_text(&"Long paragraph text running on. ".repeat(160)); let n = d.para(StoryRef::Body, &Path::top(0)).unwrap().len(); @@ -423,6 +432,7 @@ fn page_references_find_bookmarks_in_split_paragraphs_tables_and_roman_sections( /// A text box in a footer resolves its page references; one in the body keeps its result. #[test] fn page_references_in_footer_text_boxes_are_resolved() { + crate::test_fonts(); use wordcraft_doc::para::{Float, ShapeKind, Wrap}; let text_box = |d: &mut Document| { let mut p = Paragraph::new(); @@ -475,15 +485,17 @@ fn page_references_in_footer_text_boxes_are_resolved() { #[test] fn widow_control_moves_a_three_line_paragraph_whole() { + crate::test_fonts(); // A three-line paragraph with room for two at the bottom of a page can't leave a widow (two // and one) nor an orphan (one and two): Word moves it whole. A four-line one splits two and // two. Sweep the paragraph down the page a point at a time and check every split. + // Shift the filler immediately before the probe, so the sweep tests partial-line room. let mut split4 = false; for (want, words) in [(3usize, 45usize), (4, 55)] { for (fill, shift) in (30usize..60).flat_map(|f| (0..15).step_by(2).map(move |s| (f, s))) { let filler = "Filler line.\n".repeat(fill); let mut d = Document::from_text(&format!("{filler}{}\nAfter.", "word ".repeat(words))); - d.format_paragraphs(&Pos::body(0, 0), &Pos::body(0, 0), &|p| p.space_before = Some(shift as f32)).unwrap(); + d.format_paragraphs(&Pos::body(fill - 1, 0), &Pos::body(fill - 1, 0), &|p| p.space_before = Some(shift as f32)).unwrap(); let l = lay(&d); let pieces: Vec<(usize, usize)> = l .pages diff --git a/crates/layout/src/tests_styleref.rs b/crates/layout/src/tests_styleref.rs index cba5d9f8c5..5e549eda0b 100644 --- a/crates/layout/src/tests_styleref.rs +++ b/crates/layout/src/tests_styleref.rs @@ -52,6 +52,7 @@ const CHAPTERS: &[(&str, &str, bool)] = &[ #[test] fn each_page_s_header_shows_its_chapter() { + crate::test_fonts(); let d = doc(CHAPTERS, &[r#"STYLEREF "Heading 1" \* MERGEFORMAT"#, "STYLEREF 1", "STYLEREF heading1 \\l"]); let h = headers(&d); assert_eq!(h.len(), 4); @@ -62,6 +63,7 @@ fn each_page_s_header_shows_its_chapter() { #[test] fn search_from_the_bottom_with_l() { + crate::test_fonts(); let paras = [("Heading2", "First", false), ("Heading2", "Second", false), ("Normal", "Body", false), ("Heading2", "Third", true)]; let d = doc(¶s, &[r#"STYLEREF "Heading 2""#, r#"STYLEREF "Heading 2" \l"#]); assert_eq!(headers(&d), [vec!["First", "Second"], vec!["Third", "Third"]]); @@ -69,6 +71,7 @@ fn search_from_the_bottom_with_l() { #[test] fn a_missing_style_or_no_paragraph_of_it_is_an_error() { + crate::test_fonts(); let d = doc(CHAPTERS, &[r#"STYLEREF "No Such Style""#, r#"STYLEREF "Heading 3""#, "STYLEREF 12"]); let err = styleref::NO_TEXT; assert_eq!(headers(&d)[0], [err, err, err]); @@ -77,6 +80,7 @@ fn a_missing_style_or_no_paragraph_of_it_is_an_error() { #[test] fn before_the_first_heading_the_next_one_shows() { + crate::test_fonts(); let paras = [("Normal", "Preface", false), ("Heading1", "Chapter", true)]; let d = doc(¶s, &[r#"STYLEREF "Heading 1""#, r#"STYLEREF "Heading 1" \p"#]); assert_eq!(headers(&d), [vec!["Chapter", "below"], vec!["Chapter", "below"]]); @@ -84,6 +88,7 @@ fn before_the_first_heading_the_next_one_shows() { #[test] fn character_styles_and_paragraph_numbers() { + crate::test_fonts(); let mut d = doc(CHAPTERS, &[r#"STYLEREF Strong"#, r#"STYLEREF "Heading 1" \n"#, r#"STYLEREF "Heading 2" \w"#, r#"STYLEREF "Heading 1" \s"#]); // Bold a word on page 2 with the Strong character style. d.format_range(&Pos::body(3, 8), &Pos::body(3, 11), &|c: &mut CharProps| c.style = Some("Strong".into())).unwrap(); @@ -100,6 +105,7 @@ fn character_styles_and_paragraph_numbers() { /// Editing a heading updates the headers that show it, through the paragraph cache. #[test] fn headers_follow_heading_edits() { + crate::test_fonts(); let mut d = doc(CHAPTERS, &[r#"STYLEREF "Heading 1""#]); let mut c = LayoutCache::new(); let first = layout(&d, &mut c, &LayoutOptions::default()); @@ -115,6 +121,7 @@ fn headers_follow_heading_edits() { /// documents lay out on every keystroke); an edited heading or a changed style rebuilds it. #[test] fn style_index_is_reused_until_the_body_or_styles_change() { + crate::test_fonts(); let mut d = doc(CHAPTERS, &[r#"STYLEREF "Heading 1""#]); let mut c = LayoutCache::new(); let index = |c: &LayoutCache| c.styleref.as_ref().map(|(_, _, ix)| ix.clone()).unwrap(); @@ -143,6 +150,7 @@ fn style_index_is_reused_until_the_body_or_styles_change() { /// `STYLEREF 1` finds the style named `heading 1` when its id is localized (#1286). #[test] fn heading_level_matches_a_localized_style_id() { + crate::test_fonts(); let paras: Vec<(&str, &str, bool)> = CHAPTERS.iter().map(|(s, t, b)| (if *s == "Heading1" { "berschrift1" } else { *s }, *t, *b)).collect(); let mut d = doc(¶s, &["STYLEREF 1"]); let st = d.styles.styles.iter_mut().find(|s| s.id == "Heading1").unwrap(); @@ -157,6 +165,7 @@ fn heading_level_matches_a_localized_style_id() { /// edited heading still updates the headers that show it. #[test] fn body_edits_keep_styleref_headers_cached() { + crate::test_fonts(); let mut d = doc(CHAPTERS, &[r#"STYLEREF "Heading 1""#, r#"STYLEREF "Heading 1" \p"#]); let mut c = LayoutCache::new(); let opts = LayoutOptions::default(); diff --git a/crates/layout/src/tests_textbox.rs b/crates/layout/src/tests_textbox.rs index 5147133ab8..47dd8f8b05 100644 --- a/crates/layout/src/tests_textbox.rs +++ b/crates/layout/src/tests_textbox.rs @@ -51,6 +51,7 @@ fn frame_of(l: &DocLayout, id: u32) -> Rect { #[test] fn align_text_and_text_direction_place_the_text_in_the_box() { + crate::test_fonts(); let at = |body: TextBody| { let mut d = Document::from_text("x"); let id = text_box(&mut d, 0, "Hi", 144.0, 144.0, body); @@ -81,6 +82,7 @@ fn align_text_and_text_direction_place_the_text_in_the_box() { #[test] fn linked_boxes_continue_the_text() { + crate::test_fonts(); let mut d = Document::from_text("one\ntwo"); let text = "Words that run on well past the first little box. ".repeat(6); let a = text_box(&mut d, 0, &text, 144.0, 48.0, TextBody::default()); @@ -119,6 +121,7 @@ fn linked_boxes_continue_the_text() { #[test] fn hostile_link_chains_stay_bounded() { + crate::test_fonts(); // Boxes linking in a circle show their own text; a box linking to itself too. let mut d = Document::from_text("one\ntwo\nthree"); let a = text_box(&mut d, 0, "A", 72.0, 36.0, TextBody::default()); @@ -136,6 +139,7 @@ fn hostile_link_chains_stay_bounded() { #[test] fn own_margins_and_resize_to_fit_keep_the_text_in_view() { + crate::test_fonts(); // #743: a cover page's two-line box with no margins (`lIns="0"`…) is just tall enough for // its text; with Word's default margins its second line would be cut. let two = |h: f32, body: TextBody| { diff --git a/crates/layout/src/tests_vertical.rs b/crates/layout/src/tests_vertical.rs index aaec1c9ef5..782f92cbb7 100644 --- a/crates/layout/src/tests_vertical.rs +++ b/crates/layout/src/tests_vertical.rs @@ -30,6 +30,7 @@ fn body_lines(l: &DocLayout) -> Vec<(f32, f32, TextDirection)> { #[test] fn top_to_bottom_section_stacks_lines_from_the_right() { + crate::test_fonts(); let d = vertical_doc(TextDirection::Down); let l = lay(&d); let page = &l.pages[0]; @@ -70,6 +71,7 @@ fn top_to_bottom_section_stacks_lines_from_the_right() { #[test] fn bottom_to_top_section_stacks_lines_from_the_left() { + crate::test_fonts(); let l = lay(&vertical_doc(TextDirection::Up)); assert!(body_lines(&l).iter().all(|(.., t)| *t == TextDirection::Up)); let c0 = l.caret(&Pos::body(0, 0)).unwrap(); @@ -81,6 +83,7 @@ fn bottom_to_top_section_stacks_lines_from_the_left() { #[test] fn east_asian_characters_stand_upright_in_top_to_bottom_text() { + crate::test_fonts(); let l = lay(&vertical_doc(TextDirection::Down)); let d = vertical_doc(TextDirection::Down); let draws = display::page_display(&d, &l.pages[0], &Default::default()); @@ -106,6 +109,7 @@ fn east_asian_characters_stand_upright_in_top_to_bottom_text() { #[test] fn headers_stay_horizontal_and_a_vertical_section_starts_a_page() { + crate::test_fonts(); use wordcraft_doc::section::SectionStart; let mut d = Document::from_text("Horizontal\nVertical"); // Section 1 ends after paragraph 0 (continuous), section 2 is vertical. diff --git a/crates/layout/src/wordart.rs b/crates/layout/src/wordart.rs index 0938b9b7fa..0ebcaa6d86 100644 --- a/crates/layout/src/wordart.rs +++ b/crates/layout/src/wordart.rs @@ -294,6 +294,7 @@ mod tests { #[test] fn arch_baselines_lie_on_the_arc() { + crate::test_fonts(); let frame = Rect::new(10.0, 20.0, 200.0, 100.0); let w = Warp::new(&TextWarp::new("textArchUp").unwrap(), frame, 0.25).unwrap(); let (cx, cy, rx, ry) = (110.0, 70.0, 100.0, 50.0); @@ -320,6 +321,7 @@ mod tests { #[test] fn flat_and_unknown_warps_draw_flat() { + crate::test_fonts(); let frame = Rect::new(0.0, 0.0, 100.0, 50.0); assert!(Warp::new(&TextWarp::new("textPlain").unwrap(), frame, 0.2).is_none()); assert!(Warp::new(&TextWarp::new("textCanUp").unwrap(), frame, 0.2).is_none()); diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000000..4b166bfaa4 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,28 @@ +# Layout test fonts + +Layout unit tests use only the 13 font files already pinned under `assets/fonts/`: Source Sans 3, +Source Serif 4, Inter and JetBrains Mono, with their bundled styles. Family listings also include +legacy style names such as Inter Medium. They do not load installed fonts, user font preferences, +or the optional `CRAFT_FONTS_DIR` input. No download or extra font checkout is required. + +Every layout unit test starts with `crate::test_fonts();`, before constructing its fixture or +resolving a font. This initializes the process-wide database once, including when a single test is +run by name. The `wordcraft-fonts/test-support` development feature exposes the initializer; +enabling the feature alone has no effect. Initialization fails if an application database already +exists, rather than replacing a database whose faces may be in the shaping or math caches. +Other test binaries and application builds keep their normal font discovery behavior. + +Uninstalled requested families use the normal substitution lists, restricted to this fixed set. +In particular, the default Aptos request resolves to Source Sans 3. These fonts are not Calibri +metric substitutes: Word measurements remain reference behavior, while fixture dimensions must +account for the pinned font's metrics. Do not recalibrate a fixture from the same layout behavior +it is meant to test. When changing a pagination fixture, check that undoing its original layout +fix still makes the regression test fail. + +The fixed set does not cover every script or include an OpenType math font. Tests with an +existing unavailable-script guard still take that guard. Additional script or math font fixtures +belong in `storytold/craft-fonts`, with a reproducible test input before enabling them here. + +Run the suite with `cargo test -p wordcraft-layout`. The isolated font initialization regression +runs with `cargo test -p wordcraft-fonts --features test-support --test fixed_test_fonts`. +`cargo xtask ci` also runs it through the layout crate's development dependency.