""" Tests for ribbon_builder module """ import pytest from io import StringIO from unittest.mock import Mock, patch from pyPhotoAlbum.ribbon_builder import ( build_ribbon_config, get_keyboard_shortcuts, validate_ribbon_config, print_ribbon_summary, ) class TestBuildRibbonConfig: """Tests for build_ribbon_config function""" def test_empty_class(self): """Test with a class that has no ribbon actions""" class EmptyClass: pass config = build_ribbon_config(EmptyClass) assert config == {} def test_single_action(self): """Test with a class that has one ribbon action""" class SingleAction: def my_action(self): pass my_action._ribbon_action = { "tab": "Home", "group": "File", "label": "My Action", "action": "my_action", "tooltip": "Does something", } config = build_ribbon_config(SingleAction) assert "Home" in config assert len(config["Home"]["groups"]) == 1 assert config["Home"]["groups"][0]["name"] == "File" assert len(config["Home"]["groups"][0]["actions"]) == 1 assert config["Home"]["groups"][0]["actions"][0]["label"] == "My Action" def test_multiple_actions_same_group(self): """Test with multiple actions in the same group""" class MultiAction: def action1(self): pass action1._ribbon_action = { "tab": "Home", "group": "Edit", "label": "Action 1", "action": "action1", "tooltip": "First action", } def action2(self): pass action2._ribbon_action = { "tab": "Home", "group": "Edit", "label": "Action 2", "action": "action2", "tooltip": "Second action", } config = build_ribbon_config(MultiAction) assert "Home" in config assert len(config["Home"]["groups"]) == 1 assert config["Home"]["groups"][0]["name"] == "Edit" assert len(config["Home"]["groups"][0]["actions"]) == 2 def test_multiple_groups(self): """Test with actions in different groups""" class MultiGroup: def action1(self): pass action1._ribbon_action = { "tab": "Home", "group": "File", "label": "File Action", "action": "action1", "tooltip": "File stuff", } def action2(self): pass action2._ribbon_action = { "tab": "Home", "group": "Edit", "label": "Edit Action", "action": "action2", "tooltip": "Edit stuff", } config = build_ribbon_config(MultiGroup) assert "Home" in config assert len(config["Home"]["groups"]) == 2 group_names = [g["name"] for g in config["Home"]["groups"]] assert "File" in group_names assert "Edit" in group_names def test_multiple_tabs(self): """Test with actions in different tabs""" class MultiTab: def action1(self): pass action1._ribbon_action = { "tab": "Home", "group": "File", "label": "Home Action", "action": "action1", "tooltip": "Home stuff", } def action2(self): pass action2._ribbon_action = { "tab": "View", "group": "Zoom", "label": "View Action", "action": "action2", "tooltip": "View stuff", } config = build_ribbon_config(MultiTab) assert "Home" in config assert "View" in config def test_tab_ordering(self): """Test that tabs are ordered correctly""" class OrderedTabs: def action1(self): pass action1._ribbon_action = { "tab": "Export", "group": "Export", "label": "Export", "action": "action1", "tooltip": "Export", } def action2(self): pass action2._ribbon_action = { "tab": "Home", "group": "File", "label": "Home", "action": "action2", "tooltip": "Home", } def action3(self): pass action3._ribbon_action = { "tab": "View", "group": "Zoom", "label": "View", "action": "action3", "tooltip": "View", } config = build_ribbon_config(OrderedTabs) tab_names = list(config.keys()) # Home should come before View, View before Export assert tab_names.index("Home") < tab_names.index("View") assert tab_names.index("View") < tab_names.index("Export") def test_action_with_optional_fields(self): """Test action with optional icon and shortcut""" class WithOptional: def action(self): pass action._ribbon_action = { "tab": "Home", "group": "File", "label": "Save", "action": "save", "tooltip": "Save project", "icon": "save.png", "shortcut": "Ctrl+S", } config = build_ribbon_config(WithOptional) action = config["Home"]["groups"][0]["actions"][0] assert action["icon"] == "save.png" assert action["shortcut"] == "Ctrl+S" def test_action_without_optional_fields(self): """Test action without optional icon and shortcut""" class WithoutOptional: def action(self): pass action._ribbon_action = { "tab": "Home", "group": "File", "label": "Action", "action": "action", "tooltip": "Does stuff", } config = build_ribbon_config(WithoutOptional) action = config["Home"]["groups"][0]["actions"][0] assert action.get("icon") is None assert action.get("shortcut") is None def test_custom_tab_not_in_order(self): """Test custom tab not in predefined order""" class CustomTab: def action(self): pass action._ribbon_action = { "tab": "CustomTab", "group": "CustomGroup", "label": "Custom", "action": "action", "tooltip": "Custom action", } config = build_ribbon_config(CustomTab) assert "CustomTab" in config def test_inherited_actions(self): """Test that actions from parent classes are included""" class BaseClass: def base_action(self): pass base_action._ribbon_action = { "tab": "Home", "group": "File", "label": "Base Action", "action": "base_action", "tooltip": "From base", } class DerivedClass(BaseClass): def derived_action(self): pass derived_action._ribbon_action = { "tab": "Home", "group": "Edit", "label": "Derived Action", "action": "derived_action", "tooltip": "From derived", } config = build_ribbon_config(DerivedClass) # Should have both actions all_actions = [] for group in config["Home"]["groups"]: all_actions.extend(group["actions"]) action_names = [a["action"] for a in all_actions] assert "base_action" in action_names assert "derived_action" in action_names class TestGetKeyboardShortcuts: """Tests for get_keyboard_shortcuts function""" def test_empty_class(self): """Test with a class that has no shortcuts""" class NoShortcuts: pass shortcuts = get_keyboard_shortcuts(NoShortcuts) assert shortcuts == {} def test_single_shortcut(self): """Test with a single shortcut""" class SingleShortcut: def save(self): pass save._ribbon_action = { "tab": "Home", "group": "File", "label": "Save", "action": "save", "tooltip": "Save", "shortcut": "Ctrl+S", } shortcuts = get_keyboard_shortcuts(SingleShortcut) assert "Ctrl+S" in shortcuts assert shortcuts["Ctrl+S"] == "save" def test_multiple_shortcuts(self): """Test with multiple shortcuts""" class MultiShortcut: def save(self): pass save._ribbon_action = { "tab": "Home", "group": "File", "label": "Save", "action": "save", "tooltip": "Save", "shortcut": "Ctrl+S", } def undo(self): pass undo._ribbon_action = { "tab": "Home", "group": "Edit", "label": "Undo", "action": "undo", "tooltip": "Undo", "shortcut": "Ctrl+Z", } shortcuts = get_keyboard_shortcuts(MultiShortcut) assert len(shortcuts) == 2 assert shortcuts["Ctrl+S"] == "save" assert shortcuts["Ctrl+Z"] == "undo" def test_action_without_shortcut_ignored(self): """Test that actions without shortcuts are not included""" class MixedShortcuts: def with_shortcut(self): pass with_shortcut._ribbon_action = { "tab": "Home", "group": "File", "label": "With", "action": "with_shortcut", "tooltip": "Has shortcut", "shortcut": "Ctrl+W", } def without_shortcut(self): pass without_shortcut._ribbon_action = { "tab": "Home", "group": "File", "label": "Without", "action": "without_shortcut", "tooltip": "No shortcut", } shortcuts = get_keyboard_shortcuts(MixedShortcuts) assert len(shortcuts) == 1 assert "Ctrl+W" in shortcuts class TestValidateRibbonConfig: """Tests for validate_ribbon_config function""" def test_valid_config(self): """Test with a valid configuration""" config = { "Home": { "groups": [ { "name": "File", "actions": [ { "label": "Save", "action": "save", "tooltip": "Save project", } ], } ] } } errors = validate_ribbon_config(config) assert errors == [] def test_empty_config(self): """Test with empty config""" errors = validate_ribbon_config({}) assert errors == [] def test_config_not_dict(self): """Test with non-dict config""" errors = validate_ribbon_config("not a dict") assert len(errors) == 1 assert "must be a dictionary" in errors[0] def test_tab_data_not_dict(self): """Test with tab data that is not a dict""" config = {"Home": "not a dict"} errors = validate_ribbon_config(config) assert len(errors) == 1 assert "Tab 'Home' data must be a dictionary" in errors[0] def test_missing_groups_key(self): """Test with missing 'groups' key""" config = {"Home": {"other_key": []}} errors = validate_ribbon_config(config) assert len(errors) == 1 assert "missing 'groups' key" in errors[0] def test_groups_not_list(self): """Test with groups that is not a list""" config = {"Home": {"groups": "not a list"}} errors = validate_ribbon_config(config) assert len(errors) == 1 assert "groups must be a list" in errors[0] def test_group_not_dict(self): """Test with group that is not a dict""" config = {"Home": {"groups": ["not a dict"]}} errors = validate_ribbon_config(config) assert len(errors) == 1 assert "group 0 must be a dictionary" in errors[0] def test_group_missing_name(self): """Test with group missing name""" config = {"Home": {"groups": [{"actions": []}]}} errors = validate_ribbon_config(config) assert any("missing 'name'" in e for e in errors) def test_group_missing_actions(self): """Test with group missing actions""" config = {"Home": {"groups": [{"name": "File"}]}} errors = validate_ribbon_config(config) assert any("missing 'actions'" in e for e in errors) def test_actions_not_list(self): """Test with actions that is not a list""" config = {"Home": {"groups": [{"name": "File", "actions": "not a list"}]}} errors = validate_ribbon_config(config) assert any("actions must be a list" in e for e in errors) def test_action_not_dict(self): """Test with action that is not a dict""" config = {"Home": {"groups": [{"name": "File", "actions": ["not a dict"]}]}} errors = validate_ribbon_config(config) assert any("action 0 must be a dictionary" in e for e in errors) def test_action_missing_required_keys(self): """Test with action missing required keys""" config = { "Home": { "groups": [ { "name": "File", "actions": [ { "label": "Save" # missing 'action' and 'tooltip' } ], } ] } } errors = validate_ribbon_config(config) assert any("missing 'action'" in e for e in errors) assert any("missing 'tooltip'" in e for e in errors) def test_multiple_errors(self): """Test that multiple errors are collected""" config = { "Tab1": {"groups": [{"name": "Group1", "actions": [{"label": "A"}]}]}, # missing action and tooltip "Tab2": {"groups": "not a list"}, } errors = validate_ribbon_config(config) assert len(errors) >= 3 # At least: missing action, missing tooltip, groups not list class TestPrintRibbonSummary: """Tests for print_ribbon_summary function""" def test_print_empty_config(self): """Test printing empty config""" config = {} with patch("sys.stdout", new_callable=StringIO) as mock_stdout: print_ribbon_summary(config) output = mock_stdout.getvalue() assert "Total Tabs: 0" in output assert "Total Groups: 0" in output assert "Total Actions: 0" in output def test_print_single_tab(self): """Test printing single tab config""" config = { "Home": { "groups": [ { "name": "File", "actions": [ { "label": "Save", "action": "save", "tooltip": "Save", } ], } ] } } with patch("sys.stdout", new_callable=StringIO) as mock_stdout: print_ribbon_summary(config) output = mock_stdout.getvalue() assert "Total Tabs: 1" in output assert "Total Groups: 1" in output assert "Total Actions: 1" in output assert "Home" in output assert "File" in output assert "Save" in output def test_print_with_shortcuts(self): """Test printing actions with shortcuts""" config = { "Home": { "groups": [ { "name": "File", "actions": [ { "label": "Save", "action": "save", "tooltip": "Save", "shortcut": "Ctrl+S", } ], } ] } } with patch("sys.stdout", new_callable=StringIO) as mock_stdout: print_ribbon_summary(config) output = mock_stdout.getvalue() assert "(Ctrl+S)" in output def test_print_multiple_tabs_and_groups(self): """Test printing config with multiple tabs and groups""" config = { "Home": { "groups": [ { "name": "File", "actions": [ {"label": "New", "action": "new", "tooltip": "New"}, {"label": "Open", "action": "open", "tooltip": "Open"}, ], }, { "name": "Edit", "actions": [ {"label": "Undo", "action": "undo", "tooltip": "Undo"}, ], }, ] }, "View": { "groups": [ { "name": "Zoom", "actions": [ {"label": "Zoom In", "action": "zoom_in", "tooltip": "Zoom In"}, ], } ] }, } with patch("sys.stdout", new_callable=StringIO) as mock_stdout: print_ribbon_summary(config) output = mock_stdout.getvalue() assert "Total Tabs: 2" in output assert "Total Groups: 3" in output assert "Total Actions: 4" in output