pyPhotoAlbum/tests/test_ribbon_builder.py
Duncan Tourolle f6ed11b0bc
All checks were successful
Python CI / test (push) Successful in 1m20s
Lint / lint (push) Successful in 1m4s
Tests / test (3.11) (push) Successful in 1m27s
Tests / test (3.12) (push) Successful in 2m25s
Tests / test (3.13) (push) Successful in 2m52s
Tests / test (3.14) (push) Successful in 1m9s
black formatting
2025-11-27 23:07:16 +01:00

635 lines
18 KiB
Python

"""
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