diff --git a/crates/ui-egui/tests/it/combine_grid.rs b/crates/ui-egui/tests/it/combine_grid.rs index c6f2e3e72..0339e0b6b 100644 --- a/crates/ui-egui/tests/it/combine_grid.rs +++ b/crates/ui-egui/tests/it/combine_grid.rs @@ -530,6 +530,10 @@ fn combine_follows_the_order_shown_in_the_grid() { #[test] fn long_and_unusual_names_are_shortened_on_the_card_but_kept_whole() { + if crate::support::skip_without_glyphs("long_and_unusual_names_are_shortened_on_the_card_but_kept_whole", "en", "日本語のファイル名.pdf") + { + return; + } let long = "Überweisungsbestätigung für das dritte Quartal 2026 – Endfassung (unterschrieben).pdf"; let mut h = grid_of(vec![(long, fixture(1)), ("日本語のファイル名.pdf", fixture(1)), ("a&b 'd'.pdf", fixture(1))], vec2(1400.0, 900.0)); // The card's label is the whole name; only the drawing is shortened. diff --git a/crates/ui-egui/tests/it/commands.rs b/crates/ui-egui/tests/it/commands.rs index ff8282014..15c23c629 100644 --- a/crates/ui-egui/tests/it/commands.rs +++ b/crates/ui-egui/tests/it/commands.rs @@ -623,6 +623,9 @@ fn palette_ime_idle_frames_and_reopen_do_not_interrupt_composition() { #[test] fn palette_ime_chinese_preedit_commit_and_cancel_remain_uninterrupted() { + if crate::support::skip_without_glyphs("palette_ime_chinese_preedit_commit_and_cancel_remain_uninterrupted", "en", "你") { + return; + } let mut h = harness(); let mut interruptions = Vec::new(); h.state_mut().run_command("view.palette"); diff --git a/crates/ui-egui/tests/it/content.rs b/crates/ui-egui/tests/it/content.rs index 0353a7d2b..4a7927f69 100644 --- a/crates/ui-egui/tests/it/content.rs +++ b/crates/ui-egui/tests/it/content.rs @@ -335,6 +335,9 @@ fn a_picture_on_the_clipboard_is_pasted_onto_the_page() { /// written to the page as `?` (or lost). #[test] fn text_the_standard_fonts_cant_draw_stays_in_the_editor() { + if crate::support::skip_without_glyphs("text_the_standard_fonts_cant_draw_stays_in_the_editor", "en", "Hello 世界") { + return; + } let _gpu = gpu(); let mut h = harness(); assert!(h.state_mut().execute("edit.text")); @@ -372,6 +375,9 @@ fn text_the_standard_fonts_cant_draw_stays_in_the_editor() { /// is left blocking Save out of sight. #[test] fn a_kept_draft_goes_with_its_deleted_page() { + if crate::support::skip_without_glyphs("a_kept_draft_goes_with_its_deleted_page", "en", "日本語") { + return; + } let mut h = harness(); assert!(h.state_mut().apply_edit(pdfcraft_engine::Edit::InsertBlankPage { at: 1, width: 612.0, height: 792.0 })); h.run_steps(2); diff --git a/crates/ui-egui/tests/it/control.rs b/crates/ui-egui/tests/it/control.rs index e18c7bf47..fe96bb595 100644 --- a/crates/ui-egui/tests/it/control.rs +++ b/crates/ui-egui/tests/it/control.rs @@ -504,6 +504,9 @@ fn language_switch_preserves_document_and_command_ids() { assert_eq!(documents[0]["dirty"], true); let commands = ok(&mut h, &c, "ui.commands", json!({})); for code in ["ja", "zh-hans", "fr", "de", "uk", "lt", "en"] { + if crate::support::skip_without_glyphs(&format!("language_switch_preserves_document_and_command_ids ({code})"), code, "") { + continue; + } ok(&mut h, &c, "ui.set", json!({ "key": "language", "value": code })); h.run_steps(2); let state = ok(&mut h, &c, "ui.state", json!({})); @@ -908,6 +911,9 @@ fn state_and_view_options() { #[test] fn japanese_controls_and_search_keep_command_ids() { + if crate::support::skip_without_glyphs("japanese_controls_and_search_keep_command_ids", "ja", "") { + return; + } let (mut h, c) = harness(); ok(&mut h, &c, "ui.set", json!({ "key": "language", "value": "ja" })); assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["language"], "ja"); @@ -1002,6 +1008,9 @@ fn ukrainian_preferences_and_search_keep_command_ids() { #[test] fn preferences_menu_and_shortcut_allow_switching_interface_languages() { + if crate::support::skip_without_glyphs("preferences_menu_and_shortcut_allow_switching_interface_languages", "ja", "") { + return; + } let (mut h, c) = harness(); ok(&mut h, &c, "ui.set", json!({ "key": "language", "value": "ja" })); ok(&mut h, &c, "ui.click", json!({ "label": "メニュー" })); @@ -1044,6 +1053,9 @@ fn preferences_menu_and_shortcut_allow_switching_interface_languages() { #[test] fn japanese_dialogs_errors_and_custom_action_names() { + if crate::support::skip_without_glyphs("japanese_dialogs_errors_and_custom_action_names", "ja", "") { + return; + } let (mut h, c) = harness(); ok(&mut h, &c, "ui.set", json!({"key": "language", "value": "ja"})); for (dialog, label) in [ @@ -1084,6 +1096,9 @@ fn japanese_dialogs_errors_and_custom_action_names() { #[test] fn simplified_chinese_about_tabs_and_credit_controls() { + if crate::support::skip_without_glyphs("simplified_chinese_about_tabs_and_credit_controls", "zh-hans", "") { + return; + } let (mut h, c) = harness(); let documents = ok(&mut h, &c, "ui.state", json!({}))["documents"].clone(); ok(&mut h, &c, "ui.set", json!({"key": "language", "value": "zh-hans"})); @@ -1113,6 +1128,9 @@ fn simplified_chinese_about_tabs_and_credit_controls() { #[test] fn simplified_chinese_signature_prompts_and_errors_keep_document_state() { + if crate::support::skip_without_glyphs("simplified_chinese_signature_prompts_and_errors_keep_document_state", "zh-hans", "") { + return; + } let (mut h, c) = harness(); let documents = ok(&mut h, &c, "ui.state", json!({}))["documents"].clone(); ok(&mut h, &c, "ui.set", json!({"key": "language", "value": "zh-hans"})); @@ -1721,6 +1739,9 @@ fn control_quit_retains_inactive_fill_sign_typing_after_cancel() { #[test] fn control_quit_retains_blocked_content_typing_after_cancel() { + if crate::support::skip_without_glyphs("control_quit_retains_blocked_content_typing_after_cancel", "en", "Keep 世界") { + return; + } let (mut h, c) = harness(); ok(&mut h, &c, "ui.command", json!({"id":"edit.text"})); h.run_steps(3); diff --git a/crates/ui-egui/tests/it/date_format.rs b/crates/ui-egui/tests/it/date_format.rs index 0606e021d..c2dadb28d 100644 --- a/crates/ui-egui/tests/it/date_format.rs +++ b/crates/ui-egui/tests/it/date_format.rs @@ -133,6 +133,13 @@ fn month_names_follow_the_date_language_or_else_the_interface_language() { #[test] fn a_date_the_pdf_cannot_hold_is_refused_with_a_warning_first() { + if crate::support::skip_without_glyphs( + "a_date_the_pdf_cannot_hold_is_refused_with_a_warning_first", + "en", + "日曜日月曜日火曜日水曜日木曜日金曜日土曜日", + ) { + return; + } let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { let mut app = PdfCraftApp::new(); app.set_option("language", "en").unwrap(); diff --git a/crates/ui-egui/tests/it/editing.rs b/crates/ui-egui/tests/it/editing.rs index 9bf7f7081..0895a8b5b 100644 --- a/crates/ui-egui/tests/it/editing.rs +++ b/crates/ui-egui/tests/it/editing.rs @@ -1087,6 +1087,9 @@ fn save_prompt_fits_the_smallest_window_whatever_the_name() { let size = egui::vec2(820.0, 520.0); for name in names { let start: String = name.chars().take(10).collect(); + if support::skip_without_glyphs(&format!("save_prompt_fits_the_smallest_window_whatever_the_name ({start}…)"), "en", &name) { + continue; + } let mut h = Harness::builder().with_size(size).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); app.set_option("language", "en").unwrap(); @@ -3575,6 +3578,9 @@ fn an_applied_paragraph_keeps_its_edited_look_until_the_page_is_redrawn() { /// as a form field does; the editor used to close with the error and the typing was lost. #[test] fn a_paragraph_whose_edit_fails_stays_open_with_its_typing() { + if crate::support::skip_without_glyphs("a_paragraph_whose_edit_fails_stays_open_with_its_typing", "en", "Top ก") { + return; + } let bytes = source_font_fixture_with_text("BT /F1 24 Tf 20 220 Td (Top) Tj ET BT /F2 24 Tf 20 120 Td (Bottom) Tj ET"); let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); diff --git a/crates/ui-egui/tests/it/pickers.rs b/crates/ui-egui/tests/it/pickers.rs index 6aa551552..f9e46c5f2 100644 --- a/crates/ui-egui/tests/it/pickers.rs +++ b/crates/ui-egui/tests/it/pickers.rs @@ -963,6 +963,9 @@ impl Drop for AttachmentDir { #[test] fn attachment_save_preserves_bytes_metadata_and_selected_destination() { + if crate::support::skip_without_glyphs("attachment_save_preserves_bytes_metadata_and_selected_destination", "en", "user chosen 資料.bin") { + return; + } let dir = AttachmentDir::new(); let chosen = dir.0.join("user chosen 資料.bin"); let original = attachment_fixture(); diff --git a/crates/ui-egui/tests/it/support/mod.rs b/crates/ui-egui/tests/it/support/mod.rs index aa96ab20d..174ec5cc0 100644 --- a/crates/ui-egui/tests/it/support/mod.rs +++ b/crates/ui-egui/tests/it/support/mod.rs @@ -1,7 +1,12 @@ //! State-based waits for native preparation, which completes between UI frames, and pointer -//! input in the frames the platform delivers it in ([`input`]). +//! input in the frames the platform delivers it in ([`input`]), and the skip for tests whose text +//! the interface fonts cannot draw ([`skip_without_glyphs`]). +use std::collections::BTreeSet; + +use egui::FontId; +use egui::epaint::text::{Fonts, TextOptions}; use egui_kittest::Harness; -use pdfcraft_ui_egui::PdfCraftApp; +use pdfcraft_ui_egui::{PdfCraftApp, i18n, theme}; pub mod input; @@ -45,3 +50,35 @@ pub fn wait_until(h: &mut Harness<'static, PdfCraftApp>, what: &str, ready: impl h.state().toast ); } + +/// Whether the test `name` must skip because the interface fonts lack a glyph for a character it +/// draws: one in `text` or in the catalog of the interface `language`. egui 0.37 test harnesses +/// panic on a missing glyph, and Japanese, Chinese and Thai faces come only from the optional +/// craft-fonts build input or a face installed on this machine. Prints the skip line. +/// +/// The fonts are those the app installs for `language` and for the default CJK preference: a +/// switch to Chinese installs its fonts for the next frame, so the first frame in Chinese draws +/// with the fonts of the language before it. +/// +/// A build that requires craft-fonts (`CRAFT_FONTS_REQUIRED=1`, as the CI gate sets) never skips: +/// a missing glyph there is a failure. +pub fn skip_without_glyphs(name: &str, language: &str, text: &str) -> bool { + if std::env::var("CRAFT_FONTS_REQUIRED").as_deref() == Ok("1") { + return false; + } + let catalog = i18n::LANGUAGES.iter().find(|l| l.code == language).map_or("", |l| l.source); + let translations = catalog.lines().filter(|l| !l.starts_with('#')).filter_map(|l| l.split('\t').nth(2)); + let chars: BTreeSet = std::iter::once(text).chain(translations).flat_map(str::chars).filter(|c| !c.is_control()).collect(); + let cjk = theme::CjkPreference::for_language(language); + let missing = [theme::CjkPreference::Default, cjk].into_iter().find_map(|cjk| { + let defs = theme::installed_font_definitions(cjk); + let families: Vec = defs.families.keys().map(|f| FontId::new(13.0, f.clone())).collect(); + let mut fonts = Fonts::new(TextOptions::default(), defs); + chars.iter().copied().find(|c| families.iter().any(|id| !fonts.has_glyphs(id, &c.to_string()))) + }); + let Some(missing) = missing else { + return false; + }; + eprintln!("skipping {name}: no interface font draws {missing:?} (set CRAFT_FONTS_DIR to a craft-fonts checkout with a face for it)"); + true +}