mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-14 03:31:50 +02:00
674 lines
24 KiB
Python
674 lines
24 KiB
Python
from typing import Literal
|
||
|
||
import pytest
|
||
from qtpy.QtCore import QPoint, Qt
|
||
from qtpy.QtGui import QContextMenuEvent
|
||
from qtpy.QtWidgets import QComboBox, QLabel, QMenu, QStyle, QToolButton, QWidget
|
||
|
||
from bec_widgets.utils.toolbar import (
|
||
DeviceSelectionAction,
|
||
ExpandableMenuAction,
|
||
IconAction,
|
||
LongPressToolButton,
|
||
MaterialIconAction,
|
||
ModularToolBar,
|
||
QtIconAction,
|
||
SeparatorAction,
|
||
SwitchableToolBarAction,
|
||
ToolbarBundle,
|
||
WidgetAction,
|
||
)
|
||
|
||
|
||
@pytest.fixture
|
||
def dummy_widget(qtbot):
|
||
"""Fixture to create a simple widget to be used as target widget."""
|
||
widget = QWidget()
|
||
qtbot.addWidget(widget)
|
||
qtbot.waitExposed(widget)
|
||
return widget
|
||
|
||
|
||
@pytest.fixture(params=["horizontal", "vertical"])
|
||
def toolbar_fixture(qtbot, request, dummy_widget):
|
||
"""Parametrized fixture to create a ModularToolBar with different orientations."""
|
||
orientation: Literal["horizontal", "vertical"] = request.param
|
||
toolbar = ModularToolBar(
|
||
target_widget=dummy_widget,
|
||
orientation=orientation,
|
||
background_color="rgba(255, 255, 255, 255)", # White background for testing
|
||
)
|
||
qtbot.addWidget(toolbar)
|
||
qtbot.waitExposed(toolbar)
|
||
yield toolbar
|
||
toolbar.close()
|
||
|
||
|
||
@pytest.fixture
|
||
def separator_action():
|
||
"""Fixture to create a SeparatorAction."""
|
||
return SeparatorAction()
|
||
|
||
|
||
@pytest.fixture
|
||
def icon_action():
|
||
"""Fixture to create an IconAction."""
|
||
return IconAction(icon_path="assets/BEC-Icon.png", tooltip="Test Icon Action", checkable=True)
|
||
|
||
|
||
@pytest.fixture
|
||
def material_icon_action():
|
||
"""Fixture to create a MaterialIconAction."""
|
||
return MaterialIconAction(
|
||
icon_name="home", tooltip="Test Material Icon Action", checkable=False
|
||
)
|
||
|
||
|
||
@pytest.fixture
|
||
def qt_icon_action():
|
||
"""Fixture to create a QtIconAction."""
|
||
return QtIconAction(standard_icon=QStyle.SP_FileIcon, tooltip="Qt File", checkable=True)
|
||
|
||
|
||
@pytest.fixture
|
||
def device_selection_action():
|
||
"""Fixture to create a DeviceSelectionAction."""
|
||
device_combobox = QComboBox()
|
||
device_combobox.addItems(["Device 1", "Device 2", "Device 3"])
|
||
device_combobox.setCurrentIndex(0)
|
||
return DeviceSelectionAction(label="Select Device:", device_combobox=device_combobox)
|
||
|
||
|
||
@pytest.fixture
|
||
def widget_action():
|
||
"""Fixture to create a WidgetAction."""
|
||
sample_widget = QLabel("Sample Widget")
|
||
return WidgetAction(label="Sample Label:", widget=sample_widget)
|
||
|
||
|
||
@pytest.fixture
|
||
def expandable_menu_action():
|
||
"""Fixture to create an ExpandableMenuAction."""
|
||
action1 = MaterialIconAction(icon_name="counter_1", tooltip="Menu Action 1", checkable=False)
|
||
action2 = MaterialIconAction(icon_name="counter_2", tooltip="Menu Action 2", checkable=True)
|
||
actions = {"action1": action1, "action2": action2}
|
||
return ExpandableMenuAction(
|
||
label="Expandable Menu", actions=actions, icon_path="assets/BEC-Icon.png"
|
||
)
|
||
|
||
|
||
@pytest.fixture
|
||
def switchable_toolbar_action():
|
||
"""Fixture to create a switchable toolbar action with two MaterialIconActions."""
|
||
action1 = MaterialIconAction(icon_name="counter_1", tooltip="Action 1", checkable=True)
|
||
action2 = MaterialIconAction(icon_name="counter_2", tooltip="Action 2", checkable=True)
|
||
switchable = SwitchableToolBarAction(
|
||
actions={"action1": action1, "action2": action2},
|
||
initial_action="action1",
|
||
tooltip="Switchable Action",
|
||
checkable=True,
|
||
)
|
||
return switchable
|
||
|
||
|
||
def test_initialization(toolbar_fixture):
|
||
"""Test that ModularToolBar initializes correctly with different orientations."""
|
||
toolbar = toolbar_fixture
|
||
if toolbar.orientation() == Qt.Horizontal:
|
||
assert toolbar.orientation() == Qt.Horizontal
|
||
elif toolbar.orientation() == Qt.Vertical:
|
||
assert toolbar.orientation() == Qt.Vertical
|
||
else:
|
||
pytest.fail("Toolbar orientation is neither horizontal nor vertical.")
|
||
assert toolbar.background_color == "rgba(255, 255, 255, 255)"
|
||
assert toolbar.widgets == {}
|
||
assert not toolbar.isMovable()
|
||
assert not toolbar.isFloatable()
|
||
|
||
|
||
def test_set_background_color(toolbar_fixture):
|
||
"""Test setting the background color of the toolbar."""
|
||
toolbar = toolbar_fixture
|
||
new_color = "rgba(0, 0, 0, 255)" # Black
|
||
toolbar.set_background_color(new_color)
|
||
assert toolbar.background_color == new_color
|
||
# Verify stylesheet
|
||
expected_style = f"QToolBar {{ background-color: {new_color}; border: none; }}"
|
||
assert toolbar.styleSheet() == expected_style
|
||
|
||
|
||
def test_set_orientation(toolbar_fixture, qtbot, dummy_widget):
|
||
"""Test changing the orientation of the toolbar."""
|
||
toolbar = toolbar_fixture
|
||
if toolbar.orientation() == Qt.Horizontal:
|
||
new_orientation = "vertical"
|
||
else:
|
||
new_orientation = "horizontal"
|
||
toolbar.set_orientation(new_orientation)
|
||
qtbot.wait(100)
|
||
if new_orientation == "horizontal":
|
||
assert toolbar.orientation() == Qt.Horizontal
|
||
else:
|
||
assert toolbar.orientation() == Qt.Vertical
|
||
|
||
|
||
def test_add_action(
|
||
toolbar_fixture,
|
||
icon_action,
|
||
separator_action,
|
||
material_icon_action,
|
||
qt_icon_action,
|
||
dummy_widget,
|
||
):
|
||
"""Test adding different types of actions to the toolbar."""
|
||
toolbar = toolbar_fixture
|
||
|
||
# Add IconAction
|
||
toolbar.add_action("icon_action", icon_action, dummy_widget)
|
||
assert "icon_action" in toolbar.widgets
|
||
assert toolbar.widgets["icon_action"] == icon_action
|
||
assert icon_action.action in toolbar.actions()
|
||
|
||
# Add SeparatorAction
|
||
toolbar.add_action("separator_action", separator_action, dummy_widget)
|
||
assert "separator_action" in toolbar.widgets
|
||
assert toolbar.widgets["separator_action"] == separator_action
|
||
|
||
# Add MaterialIconAction
|
||
toolbar.add_action("material_icon_action", material_icon_action, dummy_widget)
|
||
assert "material_icon_action" in toolbar.widgets
|
||
assert toolbar.widgets["material_icon_action"] == material_icon_action
|
||
assert material_icon_action.action in toolbar.actions()
|
||
|
||
# Add QtIconAction
|
||
toolbar.add_action("qt_icon_action", qt_icon_action, dummy_widget)
|
||
assert "qt_icon_action" in toolbar.widgets
|
||
assert toolbar.widgets["qt_icon_action"] == qt_icon_action
|
||
assert qt_icon_action.action in toolbar.actions()
|
||
|
||
|
||
def test_hide_show_action(toolbar_fixture, icon_action, qtbot, dummy_widget):
|
||
"""Test hiding and showing actions on the toolbar."""
|
||
toolbar = toolbar_fixture
|
||
|
||
# Add an action
|
||
toolbar.add_action("icon_action", icon_action, dummy_widget)
|
||
assert icon_action.action.isVisible()
|
||
|
||
# Hide the action
|
||
toolbar.hide_action("icon_action")
|
||
qtbot.wait(100)
|
||
assert not icon_action.action.isVisible()
|
||
|
||
# Show the action
|
||
toolbar.show_action("icon_action")
|
||
qtbot.wait(100)
|
||
assert icon_action.action.isVisible()
|
||
|
||
|
||
def test_add_duplicate_action(toolbar_fixture, icon_action, dummy_widget):
|
||
"""Test that adding an action with a duplicate action_id raises a ValueError."""
|
||
toolbar = toolbar_fixture
|
||
|
||
# Add an action
|
||
toolbar.add_action("icon_action", icon_action, dummy_widget)
|
||
assert "icon_action" in toolbar.widgets
|
||
|
||
# Attempt to add another action with the same ID
|
||
with pytest.raises(ValueError) as excinfo:
|
||
toolbar.add_action("icon_action", icon_action, dummy_widget)
|
||
assert "Action with ID 'icon_action' already exists." in str(excinfo.value)
|
||
|
||
|
||
def test_update_material_icon_colors(toolbar_fixture, material_icon_action, dummy_widget):
|
||
"""Test updating the color of MaterialIconAction icons."""
|
||
toolbar = toolbar_fixture
|
||
|
||
# Add MaterialIconAction
|
||
toolbar.add_action("material_icon_action", material_icon_action, dummy_widget)
|
||
assert material_icon_action.action is not None
|
||
|
||
# Initial icon
|
||
initial_icon = material_icon_action.action.icon()
|
||
|
||
# Update color
|
||
new_color = "#ff0000" # Red
|
||
toolbar.update_material_icon_colors(new_color)
|
||
|
||
# Updated icon
|
||
updated_icon = material_icon_action.action.icon()
|
||
|
||
# Assuming that the icon changes when color is updated
|
||
assert initial_icon != updated_icon
|
||
|
||
|
||
def test_device_selection_action(toolbar_fixture, device_selection_action, dummy_widget):
|
||
"""Test adding a DeviceSelectionAction to the toolbar."""
|
||
toolbar = toolbar_fixture
|
||
toolbar.add_action("device_selection", device_selection_action, dummy_widget)
|
||
assert "device_selection" in toolbar.widgets
|
||
# DeviceSelectionAction adds a QWidget, so it should be present in the toolbar's widgets
|
||
# Check if the widget is added
|
||
widget = device_selection_action.device_combobox.parentWidget()
|
||
assert widget in toolbar.findChildren(QWidget)
|
||
# Verify that the label is correct
|
||
label = widget.findChild(QLabel)
|
||
assert label.text() == "Select Device:"
|
||
|
||
|
||
def test_widget_action(toolbar_fixture, widget_action, dummy_widget):
|
||
"""Test adding a WidgetAction to the toolbar."""
|
||
toolbar = toolbar_fixture
|
||
toolbar.add_action("widget_action", widget_action, dummy_widget)
|
||
assert "widget_action" in toolbar.widgets
|
||
# WidgetAction adds a QWidget to the toolbar
|
||
container = widget_action.widget.parentWidget()
|
||
assert container in toolbar.findChildren(QWidget)
|
||
# Verify the label if present
|
||
label = container.findChild(QLabel)
|
||
assert label.text() == "Sample Label:"
|
||
|
||
|
||
def test_expandable_menu_action(toolbar_fixture, expandable_menu_action, dummy_widget):
|
||
"""Test adding an ExpandableMenuAction to the toolbar."""
|
||
toolbar = toolbar_fixture
|
||
toolbar.add_action("expandable_menu", expandable_menu_action, dummy_widget)
|
||
assert "expandable_menu" in toolbar.widgets
|
||
# ExpandableMenuAction adds a QToolButton with a QMenu
|
||
# Find the QToolButton
|
||
tool_buttons = toolbar.findChildren(QToolButton)
|
||
assert len(tool_buttons) > 0
|
||
button = tool_buttons[-1] # Assuming it's the last one added
|
||
menu = button.menu()
|
||
assert menu is not None
|
||
# Check that menu has the correct actions
|
||
for action_id, sub_action in expandable_menu_action.actions.items():
|
||
# Check if a sub-action with the correct tooltip exists
|
||
matched = False
|
||
for menu_action in menu.actions():
|
||
if menu_action.toolTip() == sub_action.tooltip:
|
||
matched = True
|
||
break
|
||
assert matched, f"Sub-action with tooltip '{sub_action.tooltip}' not found in menu."
|
||
|
||
|
||
def test_update_material_icon_colors_no_material_actions(toolbar_fixture, dummy_widget):
|
||
"""Test updating material icon colors when there are no MaterialIconActions."""
|
||
toolbar = toolbar_fixture
|
||
# Ensure there are no MaterialIconActions
|
||
toolbar.update_material_icon_colors("#00ff00")
|
||
|
||
|
||
def test_hide_action_nonexistent(toolbar_fixture):
|
||
"""Test hiding an action that does not exist raises a ValueError."""
|
||
toolbar = toolbar_fixture
|
||
with pytest.raises(ValueError) as excinfo:
|
||
toolbar.hide_action("nonexistent_action")
|
||
assert "Action with ID 'nonexistent_action' does not exist." in str(excinfo.value)
|
||
|
||
|
||
def test_show_action_nonexistent(toolbar_fixture):
|
||
"""Test showing an action that does not exist raises a ValueError."""
|
||
toolbar = toolbar_fixture
|
||
with pytest.raises(ValueError) as excinfo:
|
||
toolbar.show_action("nonexistent_action")
|
||
assert "Action with ID 'nonexistent_action' does not exist." in str(excinfo.value)
|
||
|
||
|
||
def test_add_bundle(toolbar_fixture, dummy_widget, icon_action, material_icon_action):
|
||
"""Test adding a bundle of actions to the toolbar."""
|
||
toolbar = toolbar_fixture
|
||
bundle = ToolbarBundle(
|
||
bundle_id="test_bundle",
|
||
actions=[
|
||
("icon_action_in_bundle", icon_action),
|
||
("material_icon_in_bundle", material_icon_action),
|
||
],
|
||
)
|
||
toolbar.add_bundle(bundle, dummy_widget)
|
||
assert "test_bundle" in toolbar.bundles
|
||
assert "icon_action_in_bundle" in toolbar.widgets
|
||
assert "material_icon_in_bundle" in toolbar.widgets
|
||
assert icon_action.action in toolbar.actions()
|
||
assert material_icon_action.action in toolbar.actions()
|
||
|
||
|
||
def test_invalid_orientation(dummy_widget):
|
||
"""Test that an invalid orientation raises a ValueError."""
|
||
toolbar = ModularToolBar(target_widget=dummy_widget, orientation="horizontal")
|
||
with pytest.raises(ValueError):
|
||
toolbar.set_orientation("diagonal")
|
||
|
||
|
||
def test_widget_action_calculate_minimum_width(qtbot):
|
||
"""Test calculate_minimum_width with various combo box items."""
|
||
combo = QComboBox()
|
||
combo.addItems(["Short", "Longer Item", "The Longest Item In Combo"])
|
||
widget_action = WidgetAction(label="Test", widget=combo)
|
||
width = widget_action.calculate_minimum_width(combo)
|
||
assert width > 0
|
||
# Width should be large enough to accommodate the longest item plus additional space
|
||
assert width > 100
|
||
|
||
|
||
def test_add_action_to_bundle(toolbar_fixture, dummy_widget, material_icon_action):
|
||
# Create an initial bundle with one action
|
||
bundle = ToolbarBundle(
|
||
bundle_id="test_bundle", actions=[("initial_action", material_icon_action)]
|
||
)
|
||
toolbar_fixture.add_bundle(bundle, dummy_widget)
|
||
|
||
# Create a new action to add to the existing bundle
|
||
new_action = MaterialIconAction(
|
||
icon_name="counter_1", tooltip="New Action", checkable=True, parent=dummy_widget
|
||
)
|
||
toolbar_fixture.add_action_to_bundle("test_bundle", "new_action", new_action, dummy_widget)
|
||
|
||
# Verify the new action is registered in the toolbar's widgets
|
||
assert "new_action" in toolbar_fixture.widgets
|
||
assert toolbar_fixture.widgets["new_action"] == new_action
|
||
|
||
# Verify the new action is included in the bundle tracking
|
||
assert "new_action" in toolbar_fixture.bundles["test_bundle"]
|
||
assert toolbar_fixture.bundles["test_bundle"][-1] == "new_action"
|
||
|
||
# Verify the new action's QAction is present in the toolbar's action list
|
||
actions_list = toolbar_fixture.actions()
|
||
assert new_action.action in actions_list
|
||
|
||
# Verify that the new action is inserted immediately after the last action of the bundle
|
||
last_bundle_action = material_icon_action.action
|
||
index_last = actions_list.index(last_bundle_action)
|
||
index_new = actions_list.index(new_action.action)
|
||
assert index_new == index_last + 1
|
||
|
||
|
||
def test_context_menu_contains_added_actions(
|
||
toolbar_fixture, icon_action, material_icon_action, dummy_widget, monkeypatch
|
||
):
|
||
"""
|
||
Test that the toolbar's context menu lists all added toolbar actions.
|
||
"""
|
||
toolbar = toolbar_fixture
|
||
|
||
# Add two different actions
|
||
toolbar.add_action("icon_action", icon_action, dummy_widget)
|
||
toolbar.add_action("material_icon_action", material_icon_action, dummy_widget)
|
||
|
||
# Mock the QMenu.exec_ method to prevent the context menu from being displayed and block CI pipeline
|
||
monkeypatch.setattr(QMenu, "exec_", lambda self, pos=None: None)
|
||
event = QContextMenuEvent(QContextMenuEvent.Mouse, QPoint(10, 10))
|
||
toolbar.contextMenuEvent(event)
|
||
menus = toolbar.findChildren(QMenu)
|
||
|
||
assert len(menus) > 0
|
||
menu = menus[-1]
|
||
menu_action_texts = [action.text() for action in menu.actions()]
|
||
assert any(icon_action.tooltip in text or "icon_action" in text for text in menu_action_texts)
|
||
assert any(
|
||
material_icon_action.tooltip in text or "material_icon_action" in text
|
||
for text in menu_action_texts
|
||
)
|
||
|
||
|
||
def test_context_menu_toggle_action_visibility(
|
||
toolbar_fixture, icon_action, dummy_widget, monkeypatch
|
||
):
|
||
"""
|
||
Test that toggling action visibility works correctly through the toolbar's context menu.
|
||
"""
|
||
toolbar = toolbar_fixture
|
||
# Add an action
|
||
toolbar.add_action("icon_action", icon_action, dummy_widget)
|
||
assert icon_action.action.isVisible()
|
||
|
||
# Manually trigger the context menu event
|
||
monkeypatch.setattr(QMenu, "exec_", lambda self, pos=None: None)
|
||
event = QContextMenuEvent(QContextMenuEvent.Mouse, QPoint(10, 10))
|
||
toolbar.contextMenuEvent(event)
|
||
|
||
# Grab the menu that was created
|
||
menus = toolbar.findChildren(QMenu)
|
||
assert len(menus) > 0
|
||
menu = menus[-1]
|
||
|
||
# Locate the QAction in the menu
|
||
matching_actions = [m for m in menu.actions() if m.text() == icon_action.tooltip]
|
||
assert len(matching_actions) == 1
|
||
action_in_menu = matching_actions[0]
|
||
|
||
# Toggle it off (uncheck)
|
||
action_in_menu.setChecked(False)
|
||
menu.triggered.emit(action_in_menu)
|
||
# The action on the toolbar should now be hidden
|
||
assert not icon_action.action.isVisible()
|
||
|
||
# Toggle it on (check)
|
||
action_in_menu.setChecked(True)
|
||
menu.triggered.emit(action_in_menu)
|
||
# The action on the toolbar should be visible again
|
||
assert icon_action.action.isVisible()
|
||
|
||
|
||
def test_switchable_toolbar_action_add(toolbar_fixture, dummy_widget, switchable_toolbar_action):
|
||
"""Test that a switchable toolbar action can be added to the toolbar correctly."""
|
||
toolbar = toolbar_fixture
|
||
toolbar.add_action("switch_action", switchable_toolbar_action, dummy_widget)
|
||
|
||
# Verify the action was added correctly
|
||
assert "switch_action" in toolbar.widgets
|
||
assert toolbar.widgets["switch_action"] == switchable_toolbar_action
|
||
|
||
# Verify the button is present and is the correct type
|
||
button = switchable_toolbar_action.main_button
|
||
assert isinstance(button, LongPressToolButton)
|
||
|
||
# Verify initial state
|
||
assert switchable_toolbar_action.current_key == "action1"
|
||
assert button.toolTip() == "Action 1"
|
||
|
||
|
||
def test_switchable_toolbar_action_switching(
|
||
toolbar_fixture, dummy_widget, switchable_toolbar_action, qtbot
|
||
):
|
||
toolbar = toolbar_fixture
|
||
toolbar.add_action("switch_action", switchable_toolbar_action, dummy_widget)
|
||
# Verify initial state is set to action1
|
||
assert switchable_toolbar_action.current_key == "action1"
|
||
assert switchable_toolbar_action.main_button.toolTip() == "Action 1"
|
||
# Access the dropdown menu from the main button
|
||
menu = switchable_toolbar_action.main_button.menu()
|
||
assert menu is not None
|
||
# Find the QAction corresponding to "Action 2"
|
||
action_for_2 = None
|
||
for act in menu.actions():
|
||
if act.text() == "Action 2":
|
||
action_for_2 = act
|
||
break
|
||
assert action_for_2 is not None, "Menu action for 'Action 2' not found."
|
||
# Trigger the QAction to switch to action2
|
||
action_for_2.trigger()
|
||
qtbot.wait(100)
|
||
# Verify that the switchable action has updated its state
|
||
assert switchable_toolbar_action.current_key == "action2"
|
||
assert switchable_toolbar_action.main_button.toolTip() == "Action 2"
|
||
|
||
|
||
def test_long_pressbutton(toolbar_fixture, dummy_widget, switchable_toolbar_action, qtbot):
|
||
toolbar = toolbar_fixture
|
||
toolbar.add_action("switch_action", switchable_toolbar_action, dummy_widget)
|
||
|
||
# Verify the button is a LongPressToolButton
|
||
button = switchable_toolbar_action.main_button
|
||
assert isinstance(button, LongPressToolButton)
|
||
|
||
# Override showMenu() to record when it is called.
|
||
call_flag = []
|
||
|
||
# had to put some fake menu, we cannot call .isVisible at CI
|
||
def fake_showMenu():
|
||
call_flag.append(True)
|
||
|
||
button.showMenu = fake_showMenu
|
||
|
||
# Simulate a long press (exceeding the threshold, default 500ms).
|
||
qtbot.mousePress(button, Qt.LeftButton)
|
||
qtbot.wait(600) # wait longer than long_press_threshold
|
||
qtbot.mouseRelease(button, Qt.LeftButton)
|
||
|
||
# Verify that fake_showMenu() was called.
|
||
assert call_flag, "Long press did not trigger showMenu() as expected."
|
||
|
||
|
||
# Additional tests for action/bundle removal
|
||
def test_remove_standalone_action(toolbar_fixture, icon_action, dummy_widget):
|
||
"""
|
||
Ensure that a standalone action is fully removed and no longer accessible.
|
||
"""
|
||
toolbar = toolbar_fixture
|
||
# Add the action and check it is present
|
||
toolbar.add_action("icon_action", icon_action, dummy_widget)
|
||
assert "icon_action" in toolbar.widgets
|
||
assert icon_action.action in toolbar.actions()
|
||
|
||
# Now remove it
|
||
toolbar.remove_action("icon_action")
|
||
|
||
# Action bookkeeping
|
||
assert "icon_action" not in toolbar.widgets
|
||
# QAction list
|
||
assert icon_action.action not in toolbar.actions()
|
||
# Attempting to hide / show it should raise
|
||
with pytest.raises(ValueError):
|
||
toolbar.hide_action("icon_action")
|
||
with pytest.raises(ValueError):
|
||
toolbar.show_action("icon_action")
|
||
|
||
|
||
def test_remove_action_from_bundle(
|
||
toolbar_fixture, dummy_widget, icon_action, material_icon_action
|
||
):
|
||
"""
|
||
Remove a single action that is part of a bundle and verify clean‑up.
|
||
"""
|
||
toolbar = toolbar_fixture
|
||
bundle = ToolbarBundle(
|
||
bundle_id="test_bundle",
|
||
actions=[("icon_action", icon_action), ("material_action", material_icon_action)],
|
||
)
|
||
toolbar.add_bundle(bundle, dummy_widget)
|
||
|
||
# Initial assertions
|
||
assert "test_bundle" in toolbar.bundles
|
||
assert "icon_action" in toolbar.widgets
|
||
assert "material_action" in toolbar.widgets
|
||
|
||
# Remove one action from the bundle
|
||
toolbar.remove_action("icon_action")
|
||
|
||
# icon_action should be fully gone
|
||
assert "icon_action" not in toolbar.widgets
|
||
assert icon_action.action not in toolbar.actions()
|
||
# Bundle tracking should be updated
|
||
assert "icon_action" not in toolbar.bundles["test_bundle"]
|
||
# The other action must still exist
|
||
assert "material_action" in toolbar.widgets
|
||
assert material_icon_action.action in toolbar.actions()
|
||
|
||
|
||
def test_remove_last_action_from_bundle_removes_bundle(toolbar_fixture, dummy_widget, icon_action):
|
||
"""
|
||
Removing the final action from a bundle should delete the bundle entry itself.
|
||
"""
|
||
toolbar = toolbar_fixture
|
||
bundle = ToolbarBundle(bundle_id="single_action_bundle", actions=[("only_action", icon_action)])
|
||
toolbar.add_bundle(bundle, dummy_widget)
|
||
|
||
# Sanity check
|
||
assert "single_action_bundle" in toolbar.bundles
|
||
assert "only_action" in toolbar.widgets
|
||
|
||
# Remove the sole action
|
||
toolbar.remove_action("only_action")
|
||
|
||
# Bundle should be gone
|
||
assert "single_action_bundle" not in toolbar.bundles
|
||
# QAction removed
|
||
assert icon_action.action not in toolbar.actions()
|
||
|
||
|
||
def test_remove_entire_bundle(toolbar_fixture, dummy_widget, icon_action, material_icon_action):
|
||
"""
|
||
Ensure that removing a bundle deletes all its actions and separators.
|
||
"""
|
||
toolbar = toolbar_fixture
|
||
bundle = ToolbarBundle(
|
||
bundle_id="to_remove",
|
||
actions=[("icon_action", icon_action), ("material_action", material_icon_action)],
|
||
)
|
||
toolbar.add_bundle(bundle, dummy_widget)
|
||
|
||
# Confirm bundle presence
|
||
assert "to_remove" in toolbar.bundles
|
||
|
||
# Remove the whole bundle
|
||
toolbar.remove_bundle("to_remove")
|
||
|
||
# Bundle mapping gone
|
||
assert "to_remove" not in toolbar.bundles
|
||
# All actions gone
|
||
for aid, act in [("icon_action", icon_action), ("material_action", material_icon_action)]:
|
||
assert aid not in toolbar.widgets
|
||
assert act.action not in toolbar.actions()
|
||
|
||
|
||
def test_trigger_removed_action_raises(toolbar_fixture, icon_action, dummy_widget, qtbot):
|
||
"""
|
||
Add an action, connect a mock slot, then remove the action and verify that
|
||
attempting to trigger it afterwards raises RuntimeError (since the underlying
|
||
QAction has been deleted).
|
||
"""
|
||
toolbar = toolbar_fixture
|
||
|
||
# Add the action and connect a mock slot
|
||
toolbar.add_action("icon_action", icon_action, dummy_widget)
|
||
called = []
|
||
|
||
def mock_slot():
|
||
called.append(True)
|
||
|
||
icon_action.action.triggered.connect(mock_slot)
|
||
|
||
# Trigger once to confirm connection works
|
||
icon_action.action.trigger()
|
||
assert called == [True]
|
||
|
||
# Now remove the action
|
||
toolbar.remove_action("icon_action")
|
||
# Allow deleteLater event to process
|
||
qtbot.wait(50)
|
||
|
||
# The underlying C++ object should be deleted; triggering should raise
|
||
with pytest.raises(RuntimeError):
|
||
icon_action.action.trigger()
|
||
|
||
|
||
def test_remove_nonexistent_action(toolbar_fixture):
|
||
"""
|
||
Attempting to remove an action that does not exist should raise ValueError.
|
||
"""
|
||
toolbar = toolbar_fixture
|
||
with pytest.raises(ValueError) as excinfo:
|
||
toolbar.remove_action("nonexistent_action")
|
||
assert "Action with ID 'nonexistent_action' does not exist." in str(excinfo.value)
|
||
|
||
|
||
def test_remove_nonexistent_bundle(toolbar_fixture):
|
||
"""
|
||
Attempting to remove a bundle that does not exist should raise ValueError.
|
||
"""
|
||
toolbar = toolbar_fixture
|
||
with pytest.raises(ValueError) as excinfo:
|
||
toolbar.remove_bundle("nonexistent_bundle")
|
||
assert "Bundle 'nonexistent_bundle' does not exist." in str(excinfo.value)
|