diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs index 0e2ef2db0..bf18de841 100644 --- a/crates/ui-egui/src/panels.rs +++ b/crates/ui-egui/src/panels.rs @@ -540,317 +540,7 @@ mod font_menu_tests { } #[cfg(test)] -mod font_menu_ui_tests { - use egui::vec2; - use egui_kittest::Harness; - use egui_kittest::kittest::Queryable; - use serde_json::json; - - use crate::test_window::{self, Window, click_at}; - - /// The Type tool's selection over a new text frame's text, with the Control bar on. Returns the - /// story. - fn typing() -> (Harness<'static, Window>, u64) { - typing_in("Advanced") - } - - /// [`typing`] in the workspace `name`. - fn typing_in(name: &str) -> (Harness<'static, Window>, u64) { - typing_on(name, vec2(1440.0, 900.0), false) - } - - /// [`typing_in`] a window of `size`, with the macOS window chrome (the native menu bar and the - /// title bar inside the window) when `mac`. - fn typing_on(name: &str, size: egui::Vec2, mac: bool) -> (Harness<'static, Window>, u64) { - let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); - app.run("window.workspace", json!({ "name": name })).unwrap(); - app.native_menu = mac; - app.integrated_titlebar = mac; - let mut h = test_window::open(app, size); - let app = &mut h.state_mut().app; - let r = app.run("frame.create", json!({"rect": [72, 72, 300, 200], "content": "text", "text": "Hello", "caret": true})).unwrap(); - app.run("tool.select", json!({"tool": "type"})).unwrap(); - app.run("text.select", json!({"story": r["story"], "anchor": 0, "focus": 5})).unwrap(); - h.run_steps(4); - (h, r["story"].as_u64().unwrap()) - } - - fn story_text(h: &mut Harness<'static, Window>, story: u64) -> String { - h.state_mut().app.run("story.get", json!({"story": story})).unwrap()["text"].as_str().unwrap().to_string() - } - - /// The font fields showing `family`, top to bottom. - fn fields(h: &Harness<'static, Window>, family: &str) -> Vec { - let mut r: Vec = - h.query_all_by(|n| n.role() == egui::accesskit::Role::ComboBox && n.value().as_deref() == Some(family)).map(|n| n.rect()).collect(); - r.sort_by(|a, b| a.min.y.total_cmp(&b.min.y).then(a.min.x.total_cmp(&b.min.x))); - r - } - - /// The Control bar's font field (the topmost), showing `family`. - fn field(h: &Harness<'static, Window>, family: &str) -> egui::Rect { - *fields(h, family).first().unwrap_or_else(|| panic!("a font field shows {family}")) - } - - /// The text in the focused search field. - fn search(h: &Harness<'static, Window>) -> String { - h.get_by(|n| n.role() == egui::accesskit::Role::TextInput && n.is_focused()).value().unwrap_or_default() - } - - fn family(h: &mut Harness<'static, Window>) -> String { - h.state_mut().app.run("type.selectionAttrs", json!({})).unwrap()["chars"]["fontFamily"].as_str().unwrap().to_string() - } - - /// Run the menu until the filters' font kinds are read (it says Loading fonts… meanwhile). - fn settle(h: &mut Harness<'static, Window>) { - for _ in 0..3000 { - if h.query_by_label("Loading fonts…").is_none() { - return; - } - std::thread::sleep(std::time::Duration::from_millis(10)); - h.run_steps(1); - } - panic!("the font kinds were never read"); - } - - /// Open the Control bar's font menu (showing `family`) and search for `query`. - fn open_menu(h: &mut Harness<'static, Window>, family: &str, query: &str) { - let at = field(h, family); - click_at(h, at.center()); - if !query.is_empty() { - h.event(egui::Event::Text(query.into())); - } - h.run_steps(4); - } - - #[test] - fn a_family_opens_to_its_styles_and_a_style_is_picked() { - let (mut h, _) = typing(); - let serif = designcraft_fonts::DEFAULT_FAMILY; - open_menu(&mut h, serif, "serif 4"); - // The arrow before the name opens the family; the menu stays open. - { - let at = h.get_by_label("Show Styles").rect().center(); - click_at(&mut h, at); - } - assert!(h.query_by_label("Bold Italic").is_some(), "its styles are listed"); - { - let at = h.get_by_label("Bold Italic").rect().center(); - click_at(&mut h, at); - } - let attrs = h.state_mut().app.run("type.selectionAttrs", json!({})).unwrap(); - assert_eq!((attrs["chars"]["fontFamily"].as_str(), attrs["chars"]["fontStyle"].as_str()), (Some(serif), Some("Bold Italic"))); - assert!(!h.ctx.text_edit_focused(), "picking closes the menu"); - } - - #[test] - fn picked_fonts_come_first_as_recently_used() { - let (mut h, _) = typing(); - open_menu(&mut h, designcraft_fonts::DEFAULT_FAMILY, "source sans"); - { - let at = h.get_by_label("Source Sans 3").rect().center(); - click_at(&mut h, at); - } - assert_eq!(h.state().app.session.prefs.recent_fonts, ["Source Sans 3"]); - // Reopened without a search: Source Sans 3 is listed at the top, and again in its place (on - // screen too when few fonts are installed). - open_menu(&mut h, "Source Sans 3", ""); - let recent = - h.query_all_by_label("Source Sans 3").map(|n| n.rect()).min_by(|a, b| a.min.y.total_cmp(&b.min.y)).expect("Source Sans 3 is listed"); - let search = h.get_by(|n| n.role() == egui::accesskit::Role::TextInput && n.is_focused()).rect(); - assert!(recent.min.y > search.max.y && recent.min.y - search.max.y < 80.0, "the first row, under the filters: {recent:?} vs {search:?}"); - } - - #[test] - fn the_class_filter_and_similar_fonts_narrow_the_list() { - let (mut h, _) = typing(); - open_menu(&mut h, designcraft_fonts::DEFAULT_FAMILY, ""); - { - let at = h.get_by(|n| n.label().is_some_and(|l| l.starts_with("All Classes"))).rect().center(); - click_at(&mut h, at); - } - { - let at = h.get_by_label("Monospaced").rect().center(); - click_at(&mut h, at); - } - settle(&mut h); - assert!(h.query_by_label("JetBrains Mono").is_some(), "a monospaced family is listed"); - assert!(h.query_by_label(designcraft_fonts::DEFAULT_FAMILY).is_none(), "a serif isn't"); - // Back to all classes, then fonts like the serif: it leads, other serifs follow, no sans. - { - let at = h.get_by(|n| n.label().is_some_and(|l| l.starts_with("Monospaced ▾"))).rect().center(); - click_at(&mut h, at); - } - { - let at = h.get_by_label("All Classes").rect().center(); - click_at(&mut h, at); - } - h.event(egui::Event::Text("serif 4".into())); - h.run_steps(4); - let row = h.get_by_label(designcraft_fonts::DEFAULT_FAMILY).rect(); - h.hover_at(row.center()); - h.run_steps(2); - { - let at = h.get_by_label("Show Similar Fonts").rect().center(); - click_at(&mut h, at); - } - assert!(h.query_by_label(&format!("Similar to {}", designcraft_fonts::DEFAULT_FAMILY)).is_some()); - assert!(h.query_by_label("Source Sans 3").is_none() && h.query_by_label("JetBrains Mono").is_none(), "other classes aren't similar"); - assert!(h.query_by_label(designcraft_fonts::DEFAULT_FAMILY).is_some()); - } - - #[test] - fn every_font_field_reports_as_a_combo_box_showing_the_family() { - let (h, _) = typing(); - let shown = fields(&h, designcraft_fonts::DEFAULT_FAMILY); - // The Control bar's, and the hand-drawn ones (Properties, the Contextual Task Bar). - assert!(shown.len() >= 2, "{shown:?}"); - } - - #[test] - fn typing_in_the_font_menu_searches_and_never_reaches_the_story() { - let (mut h, story) = typing(); - let before = story_text(&mut h, story); - let at = field(&h, designcraft_fonts::DEFAULT_FAMILY); - click_at(&mut h, at.center()); - assert!(h.ctx.text_edit_focused(), "the search field takes the keyboard when the menu opens"); - h.event(egui::Event::Text("source sans".into())); - h.run_steps(4); - assert_eq!(search(&h), "source sans"); - assert_eq!(story_text(&mut h, story), before, "the typing went to the search, not the story"); - assert!(h.query_by_label("Source Sans 3").is_some(), "a match is listed"); - assert!(h.query_by_label(designcraft_fonts::DEFAULT_FAMILY).is_none(), "families that don't match aren't"); - // ↓ and Return pick the first match and close the menu. - h.key_press(egui::Key::ArrowDown); - h.run_steps(2); - h.key_press(egui::Key::Enter); - h.run_steps(4); - assert_eq!(family(&mut h), "Source Sans 3"); - assert_eq!(story_text(&mut h, story), before); - assert!(!h.ctx.text_edit_focused(), "the menu closed"); - // Reopened, the search starts empty. - let at = field(&h, "Source Sans 3"); - click_at(&mut h, at.center()); - assert_eq!(search(&h), "", "the search starts empty"); - // A click on a family picks it. - h.event(egui::Event::Text("serif 4".into())); - h.run_steps(4); - let row = h.get_by_label(designcraft_fonts::DEFAULT_FAMILY).rect(); - click_at(&mut h, row.center()); - assert_eq!(family(&mut h), designcraft_fonts::DEFAULT_FAMILY); - // Escape closes the menu and leaves the text as it was. - let at = field(&h, designcraft_fonts::DEFAULT_FAMILY); - click_at(&mut h, at.center()); - // A star marks a favourite and keeps the menu open. - h.event(egui::Event::Text("serif 4".into())); - h.run_steps(4); - let row = h.get_by_label(designcraft_fonts::DEFAULT_FAMILY).rect(); - click_at(&mut h, row.min + vec2(10.0, 12.0)); - assert!(h.state().app.session.prefs.favorite_fonts.iter().any(|f| f == designcraft_fonts::DEFAULT_FAMILY)); - assert_eq!(search(&h), "serif 4", "the menu stays open"); - h.key_press(egui::Key::Escape); - h.run_steps(4); - assert!(!h.ctx.text_edit_focused(), "the menu closed"); - assert_eq!(story_text(&mut h, story), before); - assert!(h.state().app.session.active().unwrap().selection.text.is_some(), "the text is still selected"); - } - - /// Opens the font menu of the field at `at`, clicks its search field, types, checks the menu - /// stays open and lists only the matches, then picks the first with ↓ and Return. - fn search_in_menu(h: &mut Harness<'static, Window>, at: egui::Rect, what: &str) { - click_at(h, at.center()); - let field = h.query_by(|n| n.role() == egui::accesskit::Role::TextInput && n.is_focused()).map(|n| n.rect()); - let field = field.unwrap_or_else(|| panic!("{what}: the menu opens with the search field focused")); - click_at(h, field.center()); - assert!(h.ctx.text_edit_focused(), "{what}: a click on the search field keeps the menu open"); - h.event(egui::Event::Text("SOURCE SA".into())); - h.run_steps(4); - assert_eq!(search(h), "SOURCE SA", "{what}: the typing went to the search"); - assert!(h.query_by_label("Source Sans 3").is_some(), "{what}: a match is listed"); - assert!(h.query_by_label(designcraft_fonts::DEFAULT_FAMILY).is_none(), "{what}: families that don't match aren't"); - h.key_press(egui::Key::ArrowDown); - h.run_steps(2); - h.key_press(egui::Key::Enter); - h.run_steps(4); - assert!(!h.ctx.text_edit_focused(), "{what}: the menu closed"); - } - - /// Every font field over selected text (Control bar, Contextual Task Bar, Properties › - /// Character, Character panel) searches, in the workspaces that show them and in a small - /// window with the macOS window chrome. - #[test] - fn every_font_field_searches_in_every_workspace() { - for (name, size, mac, panel) in [ - ("Advanced", vec2(1440.0, 900.0), false, None), - ("Printing and Proofing", vec2(1440.0, 900.0), false, None), - ("Essentials", vec2(1440.0, 900.0), false, Some("character")), - ("Printing and Proofing", vec2(900.0, 560.0), true, None), - ] { - let (mut h, story) = typing_on(name, size, mac); - if panel.is_some() { - h.state_mut().app.ui.open_panel = panel.map(str::to_string); - h.run_steps(4); - } - let name = &format!("{name} {size:?} {panel:?}"); - let before = story_text(&mut h, story); - let count = fields(&h, designcraft_fonts::DEFAULT_FAMILY).len(); - assert_eq!(count, 3, "{name}: the font fields are shown"); - for k in 0..count { - h.state_mut().app.run("type.char", json!({"attrs": {"fontFamily": designcraft_fonts::DEFAULT_FAMILY}})).unwrap(); - h.run_steps(4); - // A short Properties group (below it, the workspace's open panel) scrolls to the field. - let mut nodes: Vec<_> = h - .query_all_by(|n| n.role() == egui::accesskit::Role::ComboBox && n.value().as_deref() == Some(designcraft_fonts::DEFAULT_FAMILY)) - .collect(); - nodes.sort_by(|a, b| a.rect().min.y.total_cmp(&b.rect().min.y).then(a.rect().min.x.total_cmp(&b.rect().min.x))); - if let Some(node) = nodes.get(k) { - node.scroll_to_me(); - } - h.run_steps(4); - let at = fields(&h, designcraft_fonts::DEFAULT_FAMILY)[k]; - search_in_menu(&mut h, at, &format!("{name}, field {k}")); - assert_eq!(family(&mut h), "Source Sans 3", "{name}, field {k}"); - assert_eq!(story_text(&mut h, story), before, "{name}, field {k}"); - } - } - } - - /// The font fields away from the text: the Glyphs panel, Paragraph Style Options › Basic - /// Character Formats and Find/Replace Font's Replace With. - #[test] - fn font_fields_in_panels_and_dialogs_search() { - let paragraph = json!({"name": designcraft_doc::BASIC_PARAGRAPH, "section": "chars"}); - for (what, panel, dialog, shown) in [ - ("Glyphs", Some("glyphs"), None, designcraft_fonts::DEFAULT_FAMILY), - ("Paragraph Style Options", None, Some(("paragraphStyleOptions", paragraph)), designcraft_fonts::DEFAULT_FAMILY), - ( - "Find/Replace Font", - None, - Some(("findFont", json!({"toFamily": designcraft_fonts::DEFAULT_FAMILY}))), - designcraft_fonts::DEFAULT_FAMILY, - ), - ] { - let app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); - let mut h = test_window::open(app, vec2(1440.0, 900.0)); - let dialog_id = dialog.as_ref().map(|(id, _)| id.to_string()); - h.state_mut().app.ui.open_panel = panel.map(str::to_string); - h.state_mut().app.ui.dialog = dialog.map(|(id, fields)| crate::dialogs::Dialog::new(id, fields)); - h.run_steps(4); - let at = *fields(&h, shown).first().unwrap_or_else(|| panic!("{what}: the font field is shown")); - search_in_menu(&mut h, at, what); - assert_eq!(h.state().app.ui.dialog.as_ref().map(|d| d.id.clone()), dialog_id, "{what}: Return in the menu leaves the dialog open"); - assert!(!fields(&h, "Source Sans 3").is_empty(), "{what}: the field shows the family picked"); - // Escape closes the menu only. - let at = fields(&h, "Source Sans 3")[0]; - click_at(&mut h, at.center()); - assert!(h.ctx.text_edit_focused(), "{what}: the menu opened"); - h.key_press(egui::Key::Escape); - h.run_steps(4); - assert!(!h.ctx.text_edit_focused(), "{what}: Escape closed the menu"); - assert_eq!(h.state().app.ui.dialog.as_ref().map(|d| d.id.clone()), dialog_id, "{what}: Escape in the menu leaves the dialog open"); - } - } -} +mod font_menu_ui_tests; #[cfg(test)] mod swatch_list_tests { diff --git a/crates/ui-egui/src/panels/font_menu_ui_tests.rs b/crates/ui-egui/src/panels/font_menu_ui_tests.rs new file mode 100644 index 000000000..daad2d632 --- /dev/null +++ b/crates/ui-egui/src/panels/font_menu_ui_tests.rs @@ -0,0 +1,303 @@ +use egui::vec2; +use egui_kittest::Harness; +use egui_kittest::kittest::Queryable; +use serde_json::json; + +use crate::test_window::{self, Window, click_at}; + +/// The Type tool's selection over a new text frame's text, with the Control bar on. Returns the +/// story. +fn typing() -> (Harness<'static, Window>, u64) { + typing_in("Advanced") +} + +/// [`typing`] in the workspace `name`. +fn typing_in(name: &str) -> (Harness<'static, Window>, u64) { + typing_on(name, vec2(1440.0, 900.0), false) +} + +/// [`typing_in`] a window of `size`, with the macOS window chrome (the native menu bar and the +/// title bar inside the window) when `mac`. +fn typing_on(name: &str, size: egui::Vec2, mac: bool) -> (Harness<'static, Window>, u64) { + let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("window.workspace", json!({ "name": name })).unwrap(); + app.native_menu = mac; + app.integrated_titlebar = mac; + let mut h = test_window::open(app, size); + let app = &mut h.state_mut().app; + let r = app.run("frame.create", json!({"rect": [72, 72, 300, 200], "content": "text", "text": "Hello", "caret": true})).unwrap(); + app.run("tool.select", json!({"tool": "type"})).unwrap(); + app.run("text.select", json!({"story": r["story"], "anchor": 0, "focus": 5})).unwrap(); + h.run_steps(4); + (h, r["story"].as_u64().unwrap()) +} + +fn story_text(h: &mut Harness<'static, Window>, story: u64) -> String { + h.state_mut().app.run("story.get", json!({"story": story})).unwrap()["text"].as_str().unwrap().to_string() +} + +/// The font fields showing `family`, top to bottom. +fn fields(h: &Harness<'static, Window>, family: &str) -> Vec { + let mut r: Vec = + h.query_all_by(|n| n.role() == egui::accesskit::Role::ComboBox && n.value().as_deref() == Some(family)).map(|n| n.rect()).collect(); + r.sort_by(|a, b| a.min.y.total_cmp(&b.min.y).then(a.min.x.total_cmp(&b.min.x))); + r +} + +/// The Control bar's font field (the topmost), showing `family`. +fn field(h: &Harness<'static, Window>, family: &str) -> egui::Rect { + *fields(h, family).first().unwrap_or_else(|| panic!("a font field shows {family}")) +} + +/// The text in the focused search field. +fn search(h: &Harness<'static, Window>) -> String { + h.get_by(|n| n.role() == egui::accesskit::Role::TextInput && n.is_focused()).value().unwrap_or_default() +} + +fn family(h: &mut Harness<'static, Window>) -> String { + h.state_mut().app.run("type.selectionAttrs", json!({})).unwrap()["chars"]["fontFamily"].as_str().unwrap().to_string() +} + +/// Run the menu until the filters' font kinds are read (it says Loading fonts… meanwhile). +fn settle(h: &mut Harness<'static, Window>) { + for _ in 0..3000 { + if h.query_by_label("Loading fonts…").is_none() { + return; + } + std::thread::sleep(std::time::Duration::from_millis(10)); + h.run_steps(1); + } + panic!("the font kinds were never read"); +} + +/// Open the Control bar's font menu (showing `family`) and search for `query`. +fn open_menu(h: &mut Harness<'static, Window>, family: &str, query: &str) { + let at = field(h, family); + click_at(h, at.center()); + if !query.is_empty() { + h.event(egui::Event::Text(query.into())); + } + h.run_steps(4); +} + +#[test] +fn a_family_opens_to_its_styles_and_a_style_is_picked() { + let (mut h, _) = typing(); + let serif = designcraft_fonts::DEFAULT_FAMILY; + open_menu(&mut h, serif, "serif 4"); + // The arrow before the name opens the family; the menu stays open. + { + let at = h.get_by_label("Show Styles").rect().center(); + click_at(&mut h, at); + } + assert!(h.query_by_label("Bold Italic").is_some(), "its styles are listed"); + { + let at = h.get_by_label("Bold Italic").rect().center(); + click_at(&mut h, at); + } + let attrs = h.state_mut().app.run("type.selectionAttrs", json!({})).unwrap(); + assert_eq!((attrs["chars"]["fontFamily"].as_str(), attrs["chars"]["fontStyle"].as_str()), (Some(serif), Some("Bold Italic"))); + assert!(!h.ctx.text_edit_focused(), "picking closes the menu"); +} + +#[test] +fn picked_fonts_come_first_as_recently_used() { + let (mut h, _) = typing(); + open_menu(&mut h, designcraft_fonts::DEFAULT_FAMILY, "source sans"); + { + let at = h.get_by_label("Source Sans 3").rect().center(); + click_at(&mut h, at); + } + assert_eq!(h.state().app.session.prefs.recent_fonts, ["Source Sans 3"]); + // Reopened without a search: Source Sans 3 is listed at the top, and again in its place (on + // screen too when few fonts are installed). + open_menu(&mut h, "Source Sans 3", ""); + let recent = h.query_all_by_label("Source Sans 3").map(|n| n.rect()).min_by(|a, b| a.min.y.total_cmp(&b.min.y)).expect("Source Sans 3 is listed"); + let search = h.get_by(|n| n.role() == egui::accesskit::Role::TextInput && n.is_focused()).rect(); + assert!(recent.min.y > search.max.y && recent.min.y - search.max.y < 80.0, "the first row, under the filters: {recent:?} vs {search:?}"); +} + +#[test] +fn the_class_filter_and_similar_fonts_narrow_the_list() { + let (mut h, _) = typing(); + open_menu(&mut h, designcraft_fonts::DEFAULT_FAMILY, ""); + { + let at = h.get_by(|n| n.label().is_some_and(|l| l.starts_with("All Classes"))).rect().center(); + click_at(&mut h, at); + } + { + let at = h.get_by_label("Monospaced").rect().center(); + click_at(&mut h, at); + } + settle(&mut h); + assert!(h.query_by_label("JetBrains Mono").is_some(), "a monospaced family is listed"); + assert!(h.query_by_label(designcraft_fonts::DEFAULT_FAMILY).is_none(), "a serif isn't"); + // Back to all classes, then fonts like the serif: it leads, other serifs follow, no sans. + { + let at = h.get_by(|n| n.label().is_some_and(|l| l.starts_with("Monospaced ▾"))).rect().center(); + click_at(&mut h, at); + } + { + let at = h.get_by_label("All Classes").rect().center(); + click_at(&mut h, at); + } + h.event(egui::Event::Text("serif 4".into())); + h.run_steps(4); + let row = h.get_by_label(designcraft_fonts::DEFAULT_FAMILY).rect(); + h.hover_at(row.center()); + h.run_steps(2); + { + let at = h.get_by_label("Show Similar Fonts").rect().center(); + click_at(&mut h, at); + } + assert!(h.query_by_label(&format!("Similar to {}", designcraft_fonts::DEFAULT_FAMILY)).is_some()); + assert!(h.query_by_label("Source Sans 3").is_none() && h.query_by_label("JetBrains Mono").is_none(), "other classes aren't similar"); + assert!(h.query_by_label(designcraft_fonts::DEFAULT_FAMILY).is_some()); +} + +#[test] +fn every_font_field_reports_as_a_combo_box_showing_the_family() { + let (h, _) = typing(); + let shown = fields(&h, designcraft_fonts::DEFAULT_FAMILY); + // The Control bar's, and the hand-drawn ones (Properties, the Contextual Task Bar). + assert!(shown.len() >= 2, "{shown:?}"); +} + +#[test] +fn typing_in_the_font_menu_searches_and_never_reaches_the_story() { + let (mut h, story) = typing(); + let before = story_text(&mut h, story); + let at = field(&h, designcraft_fonts::DEFAULT_FAMILY); + click_at(&mut h, at.center()); + assert!(h.ctx.text_edit_focused(), "the search field takes the keyboard when the menu opens"); + h.event(egui::Event::Text("source sans".into())); + h.run_steps(4); + assert_eq!(search(&h), "source sans"); + assert_eq!(story_text(&mut h, story), before, "the typing went to the search, not the story"); + assert!(h.query_by_label("Source Sans 3").is_some(), "a match is listed"); + assert!(h.query_by_label(designcraft_fonts::DEFAULT_FAMILY).is_none(), "families that don't match aren't"); + // ↓ and Return pick the first match and close the menu. + h.key_press(egui::Key::ArrowDown); + h.run_steps(2); + h.key_press(egui::Key::Enter); + h.run_steps(4); + assert_eq!(family(&mut h), "Source Sans 3"); + assert_eq!(story_text(&mut h, story), before); + assert!(!h.ctx.text_edit_focused(), "the menu closed"); + // Reopened, the search starts empty. + let at = field(&h, "Source Sans 3"); + click_at(&mut h, at.center()); + assert_eq!(search(&h), "", "the search starts empty"); + // A click on a family picks it. + h.event(egui::Event::Text("serif 4".into())); + h.run_steps(4); + let row = h.get_by_label(designcraft_fonts::DEFAULT_FAMILY).rect(); + click_at(&mut h, row.center()); + assert_eq!(family(&mut h), designcraft_fonts::DEFAULT_FAMILY); + // Escape closes the menu and leaves the text as it was. + let at = field(&h, designcraft_fonts::DEFAULT_FAMILY); + click_at(&mut h, at.center()); + // A star marks a favourite and keeps the menu open. + h.event(egui::Event::Text("serif 4".into())); + h.run_steps(4); + let row = h.get_by_label(designcraft_fonts::DEFAULT_FAMILY).rect(); + click_at(&mut h, row.min + vec2(10.0, 12.0)); + assert!(h.state().app.session.prefs.favorite_fonts.iter().any(|f| f == designcraft_fonts::DEFAULT_FAMILY)); + assert_eq!(search(&h), "serif 4", "the menu stays open"); + h.key_press(egui::Key::Escape); + h.run_steps(4); + assert!(!h.ctx.text_edit_focused(), "the menu closed"); + assert_eq!(story_text(&mut h, story), before); + assert!(h.state().app.session.active().unwrap().selection.text.is_some(), "the text is still selected"); +} + +/// Opens the font menu of the field at `at`, clicks its search field, types, checks the menu +/// stays open and lists only the matches, then picks the first with ↓ and Return. +fn search_in_menu(h: &mut Harness<'static, Window>, at: egui::Rect, what: &str) { + click_at(h, at.center()); + let field = h.query_by(|n| n.role() == egui::accesskit::Role::TextInput && n.is_focused()).map(|n| n.rect()); + let field = field.unwrap_or_else(|| panic!("{what}: the menu opens with the search field focused")); + click_at(h, field.center()); + assert!(h.ctx.text_edit_focused(), "{what}: a click on the search field keeps the menu open"); + h.event(egui::Event::Text("SOURCE SA".into())); + h.run_steps(4); + assert_eq!(search(h), "SOURCE SA", "{what}: the typing went to the search"); + assert!(h.query_by_label("Source Sans 3").is_some(), "{what}: a match is listed"); + assert!(h.query_by_label(designcraft_fonts::DEFAULT_FAMILY).is_none(), "{what}: families that don't match aren't"); + h.key_press(egui::Key::ArrowDown); + h.run_steps(2); + h.key_press(egui::Key::Enter); + h.run_steps(4); + assert!(!h.ctx.text_edit_focused(), "{what}: the menu closed"); +} + +/// Every font field over selected text (Control bar, Contextual Task Bar, Properties › +/// Character, Character panel) searches, in the workspaces that show them and in a small +/// window with the macOS window chrome. +#[test] +fn every_font_field_searches_in_every_workspace() { + for (name, size, mac, panel) in [ + ("Advanced", vec2(1440.0, 900.0), false, None), + ("Printing and Proofing", vec2(1440.0, 900.0), false, None), + ("Essentials", vec2(1440.0, 900.0), false, Some("character")), + ("Printing and Proofing", vec2(900.0, 560.0), true, None), + ] { + let (mut h, story) = typing_on(name, size, mac); + if panel.is_some() { + h.state_mut().app.ui.open_panel = panel.map(str::to_string); + h.run_steps(4); + } + let name = &format!("{name} {size:?} {panel:?}"); + let before = story_text(&mut h, story); + let count = fields(&h, designcraft_fonts::DEFAULT_FAMILY).len(); + assert_eq!(count, 3, "{name}: the font fields are shown"); + for k in 0..count { + h.state_mut().app.run("type.char", json!({"attrs": {"fontFamily": designcraft_fonts::DEFAULT_FAMILY}})).unwrap(); + h.run_steps(4); + // A short Properties group (below it, the workspace's open panel) scrolls to the field. + let mut nodes: Vec<_> = h + .query_all_by(|n| n.role() == egui::accesskit::Role::ComboBox && n.value().as_deref() == Some(designcraft_fonts::DEFAULT_FAMILY)) + .collect(); + nodes.sort_by(|a, b| a.rect().min.y.total_cmp(&b.rect().min.y).then(a.rect().min.x.total_cmp(&b.rect().min.x))); + if let Some(node) = nodes.get(k) { + node.scroll_to_me(); + } + h.run_steps(4); + let at = fields(&h, designcraft_fonts::DEFAULT_FAMILY)[k]; + search_in_menu(&mut h, at, &format!("{name}, field {k}")); + assert_eq!(family(&mut h), "Source Sans 3", "{name}, field {k}"); + assert_eq!(story_text(&mut h, story), before, "{name}, field {k}"); + } + } +} + +/// The font fields away from the text: the Glyphs panel, Paragraph Style Options › Basic +/// Character Formats and Find/Replace Font's Replace With. +#[test] +fn font_fields_in_panels_and_dialogs_search() { + let paragraph = json!({"name": designcraft_doc::BASIC_PARAGRAPH, "section": "chars"}); + for (what, panel, dialog, shown) in [ + ("Glyphs", Some("glyphs"), None, designcraft_fonts::DEFAULT_FAMILY), + ("Paragraph Style Options", None, Some(("paragraphStyleOptions", paragraph)), designcraft_fonts::DEFAULT_FAMILY), + ("Find/Replace Font", None, Some(("findFont", json!({"toFamily": designcraft_fonts::DEFAULT_FAMILY}))), designcraft_fonts::DEFAULT_FAMILY), + ] { + let app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + let mut h = test_window::open(app, vec2(1440.0, 900.0)); + let dialog_id = dialog.as_ref().map(|(id, _)| id.to_string()); + h.state_mut().app.ui.open_panel = panel.map(str::to_string); + h.state_mut().app.ui.dialog = dialog.map(|(id, fields)| crate::dialogs::Dialog::new(id, fields)); + h.run_steps(4); + let at = *fields(&h, shown).first().unwrap_or_else(|| panic!("{what}: the font field is shown")); + search_in_menu(&mut h, at, what); + assert_eq!(h.state().app.ui.dialog.as_ref().map(|d| d.id.clone()), dialog_id, "{what}: Return in the menu leaves the dialog open"); + assert!(!fields(&h, "Source Sans 3").is_empty(), "{what}: the field shows the family picked"); + // Escape closes the menu only. + let at = fields(&h, "Source Sans 3")[0]; + click_at(&mut h, at.center()); + assert!(h.ctx.text_edit_focused(), "{what}: the menu opened"); + h.key_press(egui::Key::Escape); + h.run_steps(4); + assert!(!h.ctx.text_edit_focused(), "{what}: Escape closed the menu"); + assert_eq!(h.state().app.ui.dialog.as_ref().map(|d| d.id.clone()), dialog_id, "{what}: Escape in the menu leaves the dialog open"); + } +}