mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-01-01 03:21:19 +01:00
406 lines
16 KiB
Python
406 lines
16 KiB
Python
from unittest import mock
|
|
|
|
import pytest
|
|
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
|
|
|
from bec_widgets.utils.guided_tour import GuidedTour
|
|
from bec_widgets.utils.toolbars.actions import ExpandableMenuAction, MaterialIconAction
|
|
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
|
|
|
|
|
|
@pytest.fixture
|
|
def main_window(qtbot):
|
|
"""Create a main window for testing."""
|
|
window = QWidget()
|
|
window.resize(800, 600)
|
|
qtbot.addWidget(window)
|
|
return window
|
|
|
|
|
|
@pytest.fixture
|
|
def guided_help(main_window):
|
|
"""Create a GuidedTour instance for testing."""
|
|
return GuidedTour(main_window, enforce_visibility=False)
|
|
|
|
|
|
@pytest.fixture
|
|
def test_widget(main_window):
|
|
"""Create a test widget."""
|
|
widget = QWidget(main_window)
|
|
widget.resize(100, 50)
|
|
widget.show()
|
|
return widget
|
|
|
|
|
|
class DummyWidget(QWidget):
|
|
"""A dummy widget for testing purposes."""
|
|
|
|
def isVisible(self) -> bool:
|
|
"""Override isVisible to always return True for testing."""
|
|
return True
|
|
|
|
|
|
class TestGuidedTour:
|
|
"""Test the GuidedTour class core functionality."""
|
|
|
|
def test_initialization(self, guided_help):
|
|
"""Test GuidedTour is properly initialized."""
|
|
assert guided_help.main_window is not None
|
|
assert guided_help._registered_widgets == {}
|
|
assert guided_help._tour_steps == []
|
|
assert guided_help._current_index == 0
|
|
assert guided_help._active is False
|
|
|
|
def test_register_widget(self, guided_help: GuidedTour, test_widget: QWidget):
|
|
"""Test widget registration creates weak references."""
|
|
widget_id = guided_help.register_widget(
|
|
widget=test_widget, text="Test widget", title="TestWidget"
|
|
)
|
|
|
|
assert widget_id in guided_help._registered_widgets
|
|
registered = guided_help._registered_widgets[widget_id]
|
|
assert registered["text"] == "Test widget"
|
|
assert registered["title"] == "TestWidget"
|
|
# Check that widget_ref is callable (weak reference)
|
|
assert callable(registered["widget_ref"])
|
|
# Check that we can dereference the weak reference
|
|
assert registered["widget_ref"]() is test_widget
|
|
|
|
def test_register_widget_auto_name(self, guided_help: GuidedTour, test_widget: QWidget):
|
|
"""Test widget registration with automatic naming."""
|
|
widget_id = guided_help.register_widget(widget=test_widget, text="Test widget")
|
|
|
|
registered = guided_help._registered_widgets[widget_id]
|
|
assert registered["title"] == "QWidget"
|
|
|
|
def test_create_tour_valid_ids(self, guided_help: GuidedTour, test_widget: QWidget):
|
|
"""Test creating tour with valid widget IDs."""
|
|
widget_id = guided_help.register_widget(widget=test_widget, text="Test widget")
|
|
|
|
result = guided_help.create_tour([widget_id])
|
|
|
|
assert result is True
|
|
assert len(guided_help._tour_steps) == 1
|
|
assert guided_help._tour_steps[0]["text"] == "Test widget"
|
|
|
|
def test_create_tour_invalid_ids(self, guided_help: GuidedTour):
|
|
"""Test creating tour with invalid widget IDs."""
|
|
result = guided_help.create_tour(["invalid_id"])
|
|
|
|
assert result is False
|
|
assert len(guided_help._tour_steps) == 0
|
|
|
|
def test_start_tour_no_steps(self, guided_help: GuidedTour, test_widget: QWidget):
|
|
"""Test starting tour with no steps will add all registered widgets."""
|
|
# Register a widget
|
|
guided_help.register_widget(widget=test_widget, text="Test widget")
|
|
guided_help.start_tour()
|
|
|
|
assert guided_help._active is True
|
|
assert guided_help._current_index == 0
|
|
assert len(guided_help._tour_steps) == 1
|
|
|
|
def test_start_tour_success(self, guided_help: GuidedTour, test_widget: QWidget):
|
|
"""Test successful tour start."""
|
|
widget_id = guided_help.register_widget(widget=test_widget, text="Test widget")
|
|
guided_help.create_tour([widget_id])
|
|
|
|
guided_help.start_tour()
|
|
|
|
assert guided_help._active is True
|
|
assert guided_help._current_index == 0
|
|
assert guided_help.overlay is not None
|
|
|
|
def test_stop_tour(self, guided_help: GuidedTour, test_widget: QWidget):
|
|
"""Test stopping a tour."""
|
|
widget_id = guided_help.register_widget(widget=test_widget, text="Test widget")
|
|
guided_help.start_tour()
|
|
|
|
guided_help.stop_tour()
|
|
|
|
assert guided_help._active is False
|
|
|
|
def test_next_step(self, guided_help: GuidedTour, test_widget: QWidget):
|
|
"""Test moving to next step."""
|
|
widget1 = DummyWidget(test_widget)
|
|
widget2 = DummyWidget(test_widget)
|
|
guided_help.register_widget(widget=widget1, text="Step 1", title="Widget1")
|
|
guided_help.register_widget(widget=widget2, text="Step 2", title="Widget2")
|
|
|
|
guided_help.start_tour()
|
|
|
|
assert guided_help._current_index == 0
|
|
|
|
guided_help.next_step()
|
|
|
|
assert guided_help._current_index == 1
|
|
|
|
def test_next_step_finish_tour(self, guided_help: GuidedTour, test_widget: QWidget):
|
|
"""Test next step on last step finishes tour."""
|
|
widget_id = guided_help.register_widget(widget=test_widget, text="Test widget")
|
|
guided_help.start_tour()
|
|
|
|
guided_help.next_step()
|
|
|
|
assert guided_help._active is False
|
|
|
|
def test_prev_step(self, guided_help: GuidedTour, test_widget: QWidget):
|
|
"""Test moving to previous step."""
|
|
widget1 = DummyWidget(test_widget)
|
|
widget2 = DummyWidget(test_widget)
|
|
|
|
guided_help.register_widget(widget=widget1, text="Step 1", title="Widget1")
|
|
guided_help.register_widget(widget=widget2, text="Step 2", title="Widget2")
|
|
|
|
guided_help.start_tour()
|
|
guided_help.next_step()
|
|
|
|
assert guided_help._current_index == 1
|
|
|
|
guided_help.prev_step()
|
|
|
|
assert guided_help._current_index == 0
|
|
|
|
def test_get_registered_widgets(self, guided_help: GuidedTour, test_widget: QWidget):
|
|
"""Test getting registered widgets."""
|
|
widget_id = guided_help.register_widget(widget=test_widget, text="Test widget")
|
|
|
|
registered = guided_help.get_registered_widgets()
|
|
|
|
assert widget_id in registered
|
|
assert registered[widget_id]["text"] == "Test widget"
|
|
|
|
def test_clear_registrations(self, guided_help: GuidedTour, test_widget: QWidget):
|
|
"""Test clearing all registrations."""
|
|
guided_help.register_widget(widget=test_widget, text="Test widget")
|
|
|
|
guided_help.clear_registrations()
|
|
|
|
assert len(guided_help._registered_widgets) == 0
|
|
assert len(guided_help._tour_steps) == 0
|
|
|
|
def test_weak_reference_main_window(self, main_window: QWidget):
|
|
"""Test that main window is stored as weak reference."""
|
|
guided_help = GuidedTour(main_window)
|
|
|
|
# Should be able to get main window through weak reference
|
|
assert guided_help.main_window is not None
|
|
assert guided_help.main_window == main_window
|
|
|
|
def test_complete_tour_flow(self, guided_help: GuidedTour, test_widget: QWidget):
|
|
"""Test complete tour workflow."""
|
|
# Create widgets
|
|
widget1 = DummyWidget(test_widget)
|
|
widget2 = DummyWidget(test_widget)
|
|
|
|
# Register widgets
|
|
id1 = guided_help.register_widget(widget=widget1, text="First widget", title="Widget1")
|
|
id2 = guided_help.register_widget(widget=widget2, text="Second widget", title="Widget2")
|
|
|
|
# Create and start tour
|
|
guided_help.start_tour()
|
|
|
|
assert guided_help._active is True
|
|
assert guided_help._current_index == 0
|
|
|
|
# Move through tour
|
|
guided_help.next_step()
|
|
assert guided_help._current_index == 1
|
|
|
|
# Finish tour
|
|
guided_help.next_step()
|
|
assert guided_help._active is False
|
|
|
|
def test_finish_button_on_last_step(self, guided_help: GuidedTour, test_widget: QWidget):
|
|
"""Test that the Next button changes to Finish on the last step."""
|
|
widget1 = DummyWidget(test_widget)
|
|
widget2 = DummyWidget(test_widget)
|
|
|
|
guided_help.register_widget(widget=widget1, text="First widget", title="Widget1")
|
|
guided_help.register_widget(widget=widget2, text="Second widget", title="Widget2")
|
|
guided_help.start_tour()
|
|
|
|
overlay = guided_help.overlay
|
|
assert overlay is not None
|
|
|
|
# First step should show "Next"
|
|
assert "Next" in overlay.next_btn.text()
|
|
|
|
# Navigate to last step
|
|
guided_help.next_step()
|
|
|
|
# Last step should show "Finish"
|
|
assert "Finish" in overlay.next_btn.text()
|
|
|
|
def test_step_counter_display(self, guided_help: GuidedTour, test_widget: QWidget):
|
|
"""Test that step counter is properly displayed."""
|
|
widget1 = DummyWidget(test_widget)
|
|
widget2 = DummyWidget(test_widget)
|
|
|
|
guided_help.register_widget(widget=widget1, text="First widget", title="Widget1")
|
|
guided_help.register_widget(widget=widget2, text="Second widget", title="Widget2")
|
|
|
|
guided_help.start_tour()
|
|
|
|
overlay = guided_help.overlay
|
|
assert overlay is not None
|
|
assert overlay.step_label.text() == "Step 1 of 2"
|
|
|
|
def test_register_expandable_menu_action(self, qtbot):
|
|
"""Ensure toolbar menu actions can be registered directly."""
|
|
window = QWidget()
|
|
layout = QVBoxLayout(window)
|
|
toolbar = ModularToolBar(parent=window)
|
|
layout.addWidget(toolbar)
|
|
qtbot.addWidget(window)
|
|
|
|
tools_action = ExpandableMenuAction(
|
|
label="Tools ",
|
|
actions={
|
|
"notes": MaterialIconAction(
|
|
icon_name="note_add", tooltip="Add note", filled=True, parent=window
|
|
)
|
|
},
|
|
)
|
|
toolbar.components.add_safe("menu_tools", tools_action)
|
|
bundle = toolbar.new_bundle("menu_tools")
|
|
bundle.add_action("menu_tools")
|
|
toolbar.show_bundles(["menu_tools"])
|
|
|
|
guided = GuidedTour(window, enforce_visibility=False)
|
|
guided.register_widget(widget=tools_action, text="Toolbar tools menu")
|
|
guided.start_tour()
|
|
|
|
assert guided._active is True
|
|
|
|
@mock.patch("bec_widgets.utils.guided_tour.logger")
|
|
def test_error_handling(self, mock_logger, guided_help):
|
|
"""Test error handling and logging."""
|
|
# Test with invalid step ID
|
|
result = guided_help.create_tour(["invalid_id"])
|
|
assert result is False
|
|
mock_logger.error.assert_called()
|
|
|
|
def test_memory_safety_widget_deletion(self, guided_help: GuidedTour, test_widget: QWidget):
|
|
"""Test memory safety when widget is deleted."""
|
|
widget = QWidget(test_widget)
|
|
|
|
# Register widget
|
|
widget_id = guided_help.register_widget(widget=widget, text="Test widget")
|
|
|
|
# Verify weak reference works
|
|
registered = guided_help._registered_widgets[widget_id]
|
|
assert registered["widget_ref"]() is widget
|
|
|
|
# Delete widget
|
|
widget.close()
|
|
widget.setParent(None)
|
|
del widget
|
|
|
|
# The weak reference should now return None
|
|
# This tests that our weak reference implementation is working
|
|
assert widget_id in guided_help._registered_widgets
|
|
registered = guided_help._registered_widgets[widget_id]
|
|
assert registered["widget_ref"]() is None
|
|
|
|
def test_unregister_widget(self, guided_help: GuidedTour, test_widget: QWidget):
|
|
"""Test unregistering a widget."""
|
|
widget_id = guided_help.register_widget(widget=test_widget, text="Test widget")
|
|
|
|
# Unregister the widget
|
|
guided_help.unregister_widget(widget_id)
|
|
|
|
assert widget_id not in guided_help._registered_widgets
|
|
|
|
def test_unregister_nonexistent_widget(self, guided_help: GuidedTour):
|
|
"""Test unregistering a widget that does not exist."""
|
|
# Should not raise an error
|
|
assert guided_help.unregister_widget("nonexistent_id") is False
|
|
|
|
def test_unregister_widget_removes_from_tour(
|
|
self, guided_help: GuidedTour, test_widget: QWidget
|
|
):
|
|
"""Test that unregistering a widget also removes it from the tour steps."""
|
|
widget_id = guided_help.register_widget(widget=test_widget, text="Test widget")
|
|
guided_help.create_tour([widget_id])
|
|
|
|
# Unregister the widget
|
|
guided_help.unregister_widget(widget_id)
|
|
|
|
# The tour steps should no longer contain the unregistered widget
|
|
assert len(guided_help._tour_steps) == 0
|
|
|
|
def test_unregister_widget_during_tour_raises(
|
|
self, guided_help: GuidedTour, test_widget: QWidget
|
|
):
|
|
"""Test that unregistering a widget during an active tour raises an error."""
|
|
widget_id = guided_help.register_widget(widget=test_widget, text="Test widget")
|
|
guided_help.start_tour()
|
|
|
|
with pytest.raises(RuntimeError):
|
|
guided_help.unregister_widget(widget_id)
|
|
|
|
def test_register_lambda_function(self, guided_help: GuidedTour, test_widget: QWidget):
|
|
"""Test registering a lambda function as a widget."""
|
|
widget_id = guided_help.register_widget(
|
|
widget=lambda: (test_widget, "test text"), text="Lambda widget", title="LambdaWidget"
|
|
)
|
|
|
|
assert widget_id in guided_help._registered_widgets
|
|
registered = guided_help._registered_widgets[widget_id]
|
|
assert registered["text"] == "Lambda widget"
|
|
assert registered["title"] == "LambdaWidget"
|
|
# Check that widget_ref is callable (weak reference)
|
|
assert callable(registered["widget_ref"])
|
|
# Check that we can dereference the weak reference
|
|
assert registered["widget_ref"]()[0] is test_widget
|
|
assert registered["widget_ref"]()[1] == "test text"
|
|
|
|
def test_register_widget_local_function(self, guided_help: GuidedTour, test_widget: QWidget):
|
|
"""Test registering a local function as a widget."""
|
|
|
|
def local_widget_function():
|
|
return test_widget, "local text"
|
|
|
|
widget_id = guided_help.register_widget(
|
|
widget=local_widget_function, text="Local function widget", title="LocalWidget"
|
|
)
|
|
|
|
assert widget_id in guided_help._registered_widgets
|
|
registered = guided_help._registered_widgets[widget_id]
|
|
assert registered["text"] == "Local function widget"
|
|
assert registered["title"] == "LocalWidget"
|
|
# Check that widget_ref is callable (weak reference)
|
|
assert callable(registered["widget_ref"])
|
|
# Check that we can dereference the weak reference
|
|
assert registered["widget_ref"]()[0] is test_widget
|
|
assert registered["widget_ref"]()[1] == "local text"
|
|
|
|
def test_text_accepts_html_content(self, guided_help: GuidedTour, test_widget: QWidget, qtbot):
|
|
"""Test that registered text can contain HTML content."""
|
|
html_text = (
|
|
"<b>Bold Text</b> with <i>Italics</i> and a <a href='https://example.com'>link</a>."
|
|
)
|
|
widget_id = guided_help.register_widget(
|
|
widget=test_widget, text=html_text, title="HTMLWidget"
|
|
)
|
|
|
|
assert widget_id in guided_help._registered_widgets
|
|
registered = guided_help._registered_widgets[widget_id]
|
|
assert registered["text"] == html_text
|
|
|
|
def test_overlay_painter(self, guided_help: GuidedTour, test_widget: QWidget, qtbot):
|
|
"""
|
|
Test that the overlay painter works without errors.
|
|
While we cannot directly test the visual output, we can ensure
|
|
that calling the paintEvent does not raise exceptions.
|
|
"""
|
|
widget_id = guided_help.register_widget(
|
|
widget=test_widget, text="Test widget for overlay", title="OverlayWidget"
|
|
)
|
|
widget = guided_help._registered_widgets[widget_id]["widget_ref"]()
|
|
with mock.patch.object(widget, "isVisible", return_value=True):
|
|
guided_help.start_tour()
|
|
guided_help.overlay.paintEvent(None) # Force paint event to render text
|
|
qtbot.wait(300) # Wait for rendering
|