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
635 lines
18 KiB
Python
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
|