From 0f98dba5f82a0febbedec1b1bb3cfc393ee1d8ff Mon Sep 17 00:00:00 2001 From: Tony Bierman Date: Mon, 29 Dec 2025 17:52:48 -0600 Subject: [PATCH] Added tests for the MenuBar widget --- src/widget/menu/menu_bar.rs | 170 ++++++++++++ tests/menu_bar_integration_tests.rs | 408 ++++++++++++++++++++++++++++ 2 files changed, 578 insertions(+) create mode 100644 tests/menu_bar_integration_tests.rs diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index e880a9f5..1865c47f 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -716,3 +716,173 @@ where Self::new(value) } } + +#[cfg(test)] +mod tests { + use super::*; + use iced_widget::text::Text; + + #[derive(Clone, Debug)] + #[allow(dead_code)] + enum TestMessage { + Action1, + Action2, + } + + type TestMenuBar<'a> = MenuBar<'a, TestMessage, iced_widget::Theme, iced_widget::Renderer>; + + #[test] + fn menu_bar_new_creates_instance() { + let items = vec![Item::new(Text::new("File")), Item::new(Text::new("Edit"))]; + + let menu_bar = TestMenuBar::new(items); + assert_eq!(menu_bar.roots.len(), 2); + assert_eq!(menu_bar.spacing, Pixels::ZERO); + assert_eq!(menu_bar.width, Length::Shrink); + assert_eq!(menu_bar.height, Length::Shrink); + } + + #[test] + fn menu_bar_width_sets_value() { + let items = vec![Item::new(Text::new("File"))]; + let menu_bar = TestMenuBar::new(items).width(Length::Fill); + assert_eq!(menu_bar.width, Length::Fill); + } + + #[test] + fn menu_bar_height_sets_value() { + let items = vec![Item::new(Text::new("File"))]; + let menu_bar = TestMenuBar::new(items).height(Length::Fixed(50.0)); + assert_eq!(menu_bar.height, Length::Fixed(50.0)); + } + + #[test] + fn menu_bar_spacing_sets_value() { + let items = vec![Item::new(Text::new("File"))]; + let menu_bar = TestMenuBar::new(items).spacing(Pixels(10.0)); + assert_eq!(menu_bar.spacing, Pixels(10.0)); + } + + #[test] + fn menu_bar_padding_sets_value() { + let items = vec![Item::new(Text::new("File"))]; + let menu_bar = TestMenuBar::new(items).padding(Padding::new(5.0)); + assert_eq!(menu_bar.padding, Padding::new(5.0)); + } + + #[test] + fn menu_bar_safe_bounds_margin_sets_value() { + let items = vec![Item::new(Text::new("File"))]; + let menu_bar = TestMenuBar::new(items).safe_bounds_margin(100.0); + #[allow(clippy::float_cmp)] + { + assert_eq!(menu_bar.global_parameters.safe_bounds_margin, 100.0); + } + } + + #[test] + fn menu_bar_close_on_item_click_sets_value() { + let items = vec![Item::new(Text::new("File"))]; + let menu_bar = TestMenuBar::new(items).close_on_item_click(true); + assert_eq!(menu_bar.close_on_item_click, Some(true)); + } + + #[test] + fn menu_bar_close_on_background_click_sets_value() { + let items = vec![Item::new(Text::new("File"))]; + let menu_bar = TestMenuBar::new(items).close_on_background_click(true); + assert_eq!(menu_bar.close_on_background_click, Some(true)); + } + + #[test] + fn menu_bar_close_on_item_click_global_sets_value() { + let items = vec![Item::new(Text::new("File"))]; + let menu_bar = TestMenuBar::new(items).close_on_item_click_global(true); + assert!(menu_bar.global_parameters.close_on_item_click); + } + + #[test] + fn menu_bar_close_on_background_click_global_sets_value() { + let items = vec![Item::new(Text::new("File"))]; + let menu_bar = TestMenuBar::new(items).close_on_background_click_global(true); + assert!(menu_bar.global_parameters.close_on_background_click); + } + + #[test] + fn menu_bar_with_multiple_items() { + let items = vec![ + Item::new(Text::new("File")), + Item::new(Text::new("Edit")), + Item::new(Text::new("View")), + Item::new(Text::new("Help")), + ]; + + let menu_bar = TestMenuBar::new(items); + assert_eq!(menu_bar.roots.len(), 4); + } + + #[test] + fn menu_bar_chained_configuration() { + let items = vec![Item::new(Text::new("File"))]; + + let menu_bar = TestMenuBar::new(items) + .width(Length::Fill) + .height(Length::Fixed(40.0)) + .spacing(Pixels(5.0)) + .padding(Padding::new(10.0)) + .safe_bounds_margin(75.0) + .close_on_item_click(true) + .close_on_background_click(true); + + assert_eq!(menu_bar.width, Length::Fill); + assert_eq!(menu_bar.height, Length::Fixed(40.0)); + assert_eq!(menu_bar.spacing, Pixels(5.0)); + assert_eq!(menu_bar.padding, Padding::new(10.0)); + #[allow(clippy::float_cmp)] + { + assert_eq!(menu_bar.global_parameters.safe_bounds_margin, 75.0); + } + assert_eq!(menu_bar.close_on_item_click, Some(true)); + assert_eq!(menu_bar.close_on_background_click, Some(true)); + } + + #[test] + fn menu_bar_tag_returns_state_tag() { + let items = vec![Item::new(Text::new("File"))]; + let menu_bar = TestMenuBar::new(items); + + let tag = Widget::::tag(&menu_bar); + assert_eq!(tag, tree::Tag::of::()); + } + + #[test] + fn menu_bar_children_returns_item_trees() { + let items = vec![Item::new(Text::new("File")), Item::new(Text::new("Edit"))]; + + let menu_bar = TestMenuBar::new(items); + let children = + Widget::::children(&menu_bar); + assert_eq!(children.len(), 2); + } + + #[test] + fn menu_bar_size_returns_configured_size() { + let items = vec![Item::new(Text::new("File"))]; + let menu_bar = TestMenuBar::new(items) + .width(Length::Fill) + .height(Length::Fixed(40.0)); + + let size = + Widget::::size(&menu_bar); + assert_eq!(size.width, Length::Fill); + assert_eq!(size.height, Length::Fixed(40.0)); + } + + #[test] + fn menu_bar_converts_to_element() { + let items = vec![Item::new(Text::new("File"))]; + let menu_bar = TestMenuBar::new(items); + let _element: Element = + menu_bar.into(); + } +} diff --git a/tests/menu_bar_integration_tests.rs b/tests/menu_bar_integration_tests.rs new file mode 100644 index 00000000..fe670e20 --- /dev/null +++ b/tests/menu_bar_integration_tests.rs @@ -0,0 +1,408 @@ +//! Integration tests for the MenuBar widget +//! +//! These tests verify the MenuBar widget's behavior and public API +//! from an external perspective, testing the widget as a user of the +//! library would interact with it. + +// Simulator API https://raw.githubusercontent.com/iced-rs/iced/master/test/src/simulator.rs + +use iced::{Element, Settings}; +use iced_aw::menu::Menu; +use iced_aw::{menu_bar, menu_items}; +use iced_test::{Error, Simulator}; +use iced_widget::{button, text::Text}; + +#[derive(Clone, Debug, PartialEq)] +enum Message { + FileNew, + FileOpen, + FileSave, + EditCut, + EditCopy, +} + +type ViewFn = Box Element<'static, Message>>; + +#[derive(Clone)] +struct App { + view_fn: std::rc::Rc, +} + +impl App { + fn new(view_fn: F) -> (Self, iced::Task) + where + F: Fn() -> Element<'static, Message> + 'static, + { + ( + App { + view_fn: std::rc::Rc::new(Box::new(view_fn)), + }, + iced::Task::none(), + ) + } + + fn update(&mut self, _message: Message) { + // No state changes needed for these tests + } + + fn view(&self) -> Element<'_, Message> { + (self.view_fn)() + } +} + +fn simulator(app: &App) -> Simulator<'_, Message> { + Simulator::with_settings( + Settings { + ..Settings::default() + }, + app.view(), + ) +} + +// ============================================================================ +// Basic MenuBar Tests +// ============================================================================ + +#[test] +fn can_find_menu_bar_items() -> Result<(), Error> { + let (mut app, _) = App::new(|| { + menu_bar!( + (Text::new("File")), + (Text::new("Edit")), + (Text::new("View")) + ) + .into() + }); + + let ui = simulator(&app); + for message in ui.into_messages() { + app.update(message); + } + + let mut ui = simulator(&app); + assert!(ui.find("File").is_ok(), "Should find File menu item"); + assert!(ui.find("Edit").is_ok(), "Should find Edit menu item"); + assert!(ui.find("View").is_ok(), "Should find View menu item"); + + Ok(()) +} + +#[test] +fn menu_bar_with_single_item() -> Result<(), Error> { + let (mut app, _) = App::new(|| menu_bar!((Text::new("File"))).into()); + + let ui = simulator(&app); + for message in ui.into_messages() { + app.update(message); + } + + let mut ui = simulator(&app); + assert!(ui.find("File").is_ok(), "Should find File menu item"); + + Ok(()) +} + +#[test] +fn menu_bar_with_multiple_items() -> Result<(), Error> { + let (mut app, _) = App::new(|| { + menu_bar!( + (Text::new("File")), + (Text::new("Edit")), + (Text::new("View")), + (Text::new("Help")) + ) + .into() + }); + + let ui = simulator(&app); + for message in ui.into_messages() { + app.update(message); + } + + let mut ui = simulator(&app); + assert!(ui.find("File").is_ok(), "Should find File menu item"); + assert!(ui.find("Edit").is_ok(), "Should find Edit menu item"); + assert!(ui.find("View").is_ok(), "Should find View menu item"); + assert!(ui.find("Help").is_ok(), "Should find Help menu item"); + + Ok(()) +} + +// ============================================================================ +// Nested Menu Tests +// ============================================================================ + +#[test] +fn menu_bar_with_submenu() -> Result<(), Error> { + let (mut app, _) = App::new(|| { + let file_menu = Menu::new(menu_items!( + (button(Text::new("New")).on_press(Message::FileNew)), + (button(Text::new("Open")).on_press(Message::FileOpen)), + (button(Text::new("Save")).on_press(Message::FileSave)) + )); + + menu_bar!((Text::new("File"), file_menu)).into() + }); + + let ui = simulator(&app); + for message in ui.into_messages() { + app.update(message); + } + + let mut ui = simulator(&app); + assert!(ui.find("File").is_ok(), "Should find File menu item"); + + Ok(()) +} + +#[test] +fn clicking_menu_bar_item_shows_submenu() -> Result<(), Error> { + let (app, _) = App::new(|| { + let file_menu = Menu::new(menu_items!( + (button(Text::new("New")).on_press(Message::FileNew)), + (button(Text::new("Open")).on_press(Message::FileOpen)), + (button(Text::new("Save")).on_press(Message::FileSave)) + )); + + menu_bar!((Text::new("File"), file_menu)).into() + }); + + let mut ui = simulator(&app); + + // Click on File to open the menu + ui.click("File")?; + + // After clicking, the submenu items should be visible + assert!( + ui.find("New").is_ok(), + "Should find New menu item after clicking File" + ); + assert!( + ui.find("Open").is_ok(), + "Should find Open menu item after clicking File" + ); + assert!( + ui.find("Save").is_ok(), + "Should find Save menu item after clicking File" + ); + + Ok(()) +} + +// ============================================================================ +// Multiple Menus Tests +// ============================================================================ + +#[test] +fn menu_bar_with_multiple_menus() -> Result<(), Error> { + let (mut app, _) = App::new(|| { + let file_menu = Menu::new(menu_items!( + (button(Text::new("New")).on_press(Message::FileNew)), + (button(Text::new("Open")).on_press(Message::FileOpen)) + )); + + let edit_menu = Menu::new(menu_items!( + (button(Text::new("Cut")).on_press(Message::EditCut)), + (button(Text::new("Copy")).on_press(Message::EditCopy)) + )); + + menu_bar!( + (Text::new("File"), file_menu), + (Text::new("Edit"), edit_menu) + ) + .into() + }); + + let ui = simulator(&app); + for message in ui.into_messages() { + app.update(message); + } + + let mut ui = simulator(&app); + assert!(ui.find("File").is_ok(), "Should find File menu"); + assert!(ui.find("Edit").is_ok(), "Should find Edit menu"); + + Ok(()) +} + +// ============================================================================ +// Keyboard Navigation Tests +// ============================================================================ + +#[test] +fn keyboard_navigation_in_menu() -> Result<(), Error> { + let (app, _) = App::new(|| { + let file_menu = Menu::new(menu_items!( + (button(Text::new("New")).on_press(Message::FileNew)), + (button(Text::new("Open")).on_press(Message::FileOpen)), + (button(Text::new("Save")).on_press(Message::FileSave)) + )); + + menu_bar!((Text::new("File"), file_menu)).into() + }); + + let mut ui = simulator(&app); + + // Click File to open menu + ui.click("File")?; + + // Try keyboard navigation with arrow keys + ui.tap_key(iced::keyboard::Key::Named( + iced::keyboard::key::Named::ArrowDown, + )); + + // The menu should still be visible + assert!( + ui.find("New").is_ok(), + "Menu should still be visible after arrow key" + ); + + Ok(()) +} + +// ============================================================================ +// Click Behavior Tests +// ============================================================================ + +#[test] +fn clicking_outside_closes_menu() -> Result<(), Error> { + let (app, _) = App::new(|| { + let file_menu = Menu::new(menu_items!( + (button(Text::new("New")).on_press(Message::FileNew)) + )); + + menu_bar!((Text::new("File"), file_menu)).into() + }); + + let mut ui = simulator(&app); + + // Click File to open menu + ui.click("File")?; + assert!(ui.find("New").is_ok(), "Menu should be open"); + + // Note: Clicking outside would require the simulator to support clicking at specific coordinates + // This test is a placeholder for that functionality + + Ok(()) +} + +// ============================================================================ +// Menu Item Interaction Tests +// ============================================================================ + +#[test] +fn menu_with_buttons_and_text() -> Result<(), Error> { + let (app, _) = App::new(|| { + let file_menu = Menu::new(menu_items!( + (button(Text::new("New")).on_press(Message::FileNew)), + (Text::new("Separator")), + (button(Text::new("Open")).on_press(Message::FileOpen)) + )); + + menu_bar!((Text::new("File"), file_menu)).into() + }); + + let mut ui = simulator(&app); + + // Click File to open menu + ui.click("File")?; + + assert!(ui.find("New").is_ok(), "Should find New button"); + assert!(ui.find("Separator").is_ok(), "Should find Separator text"); + assert!(ui.find("Open").is_ok(), "Should find Open button"); + + Ok(()) +} + +// ============================================================================ +// Empty and Edge Case Tests +// ============================================================================ + +#[test] +fn menu_bar_with_empty_text() -> Result<(), Error> { + let (mut app, _) = App::new(|| menu_bar!((Text::new(""))).into()); + + let ui = simulator(&app); + for message in ui.into_messages() { + app.update(message); + } + + // Empty text should still create a menu bar item, just with no visible text + // The test verifies the menu bar doesn't crash with empty text + Ok(()) +} + +#[test] +fn menu_bar_with_unicode_text() -> Result<(), Error> { + let (mut app, _) = App::new(|| { + menu_bar!( + (Text::new("文件")), + (Text::new("编辑")), + (Text::new("🎨 View")) + ) + .into() + }); + + let ui = simulator(&app); + for message in ui.into_messages() { + app.update(message); + } + + let mut ui = simulator(&app); + assert!(ui.find("文件").is_ok(), "Should find Chinese File menu"); + assert!(ui.find("编辑").is_ok(), "Should find Chinese Edit menu"); + assert!( + ui.find("🎨 View").is_ok(), + "Should find View menu with emoji" + ); + + Ok(()) +} + +// ============================================================================ +// Configuration Tests +// ============================================================================ + +#[test] +fn menu_bar_with_custom_spacing() -> Result<(), Error> { + let (mut app, _) = App::new(|| { + menu_bar!((Text::new("File")), (Text::new("Edit"))) + .spacing(20.0) + .into() + }); + + let ui = simulator(&app); + for message in ui.into_messages() { + app.update(message); + } + + let mut ui = simulator(&app); + assert!( + ui.find("File").is_ok(), + "Should find File with custom spacing" + ); + assert!( + ui.find("Edit").is_ok(), + "Should find Edit with custom spacing" + ); + + Ok(()) +} + +#[test] +fn menu_bar_with_custom_padding() -> Result<(), Error> { + let (mut app, _) = App::new(|| menu_bar!((Text::new("File"))).padding(10.0).into()); + + let ui = simulator(&app); + for message in ui.into_messages() { + app.update(message); + } + + let mut ui = simulator(&app); + assert!( + ui.find("File").is_ok(), + "Should find File with custom padding" + ); + + Ok(()) +}