mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-03-09 18:27:52 +01:00
feat: add guided tour docs to device-manager-view
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"""Module for Device Manager View."""
|
||||
|
||||
from qtpy.QtCore import QRect
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.applications.views.device_manager_view.device_manager_widget import (
|
||||
@@ -47,30 +48,95 @@ class DeviceManagerView(ViewBase):
|
||||
step_ids = []
|
||||
dm_widget = self.device_manager_widget
|
||||
|
||||
# The device_manager_widget is not yet initialized, so we will register
|
||||
# tour steps for its uninitialized state.
|
||||
|
||||
# Register Load Current Config button
|
||||
def get_load_current():
|
||||
main_app.set_current("device_manager")
|
||||
if dm_widget._initialized is True:
|
||||
return (None, None)
|
||||
return (dm_widget.button_load_current_config, None)
|
||||
|
||||
step_id = guided_tour.register_widget(
|
||||
widget=get_load_current,
|
||||
title="Load Current Config",
|
||||
text="Load the current device configuration from the BEC server. This will display all available devices and their current status.",
|
||||
text="Load the current device configuration from the BEC server.",
|
||||
)
|
||||
step_ids.append(step_id)
|
||||
|
||||
# Register Load Config From File button
|
||||
def get_load_file():
|
||||
main_app.set_current("device_manager")
|
||||
if dm_widget._initialized is True:
|
||||
return (None, None)
|
||||
return (dm_widget.button_load_config_from_file, None)
|
||||
|
||||
step_id = guided_tour.register_widget(
|
||||
widget=get_load_file,
|
||||
title="Load Config From File",
|
||||
text="Load a device configuration from a YAML file on disk. Useful for testing or working offline.",
|
||||
text="Load a device configuration from a YAML file on disk.",
|
||||
)
|
||||
step_ids.append(step_id)
|
||||
|
||||
## Register steps for the initialized state
|
||||
# Register main device table
|
||||
def get_device_table():
|
||||
main_app.set_current("device_manager")
|
||||
if dm_widget._initialized is False:
|
||||
return (None, None)
|
||||
return (dm_widget.device_manager_display.device_table_view, None)
|
||||
|
||||
step_id = guided_tour.register_widget(
|
||||
widget=get_device_table,
|
||||
title="Device Table",
|
||||
text="This table displays the config that is prepared to be uploaded to the BEC server. It allows users to review and modify device config settings, and also validate them before uploading to the BEC server.",
|
||||
)
|
||||
step_ids.append(step_id)
|
||||
|
||||
col_text_mapping = {
|
||||
0: "Shows if a device configuration is valid. Automatically validated when adding a new device.",
|
||||
1: "Shows if a device is connectable. Validated on demand.",
|
||||
2: "Device name, unique across all devices within a config.",
|
||||
3: "Device class used to initialize the device on the BEC server.",
|
||||
4: "Defines how BEC treats readings of the device during scans. The options are 'monitored', 'baseline', ' async', 'continuous' or 'on_demand'.",
|
||||
5: "Defines how BEC reacts if a device readback fails. Options are 'raise', 'retry', or 'buffer'.",
|
||||
6: "User-defined tags associated with the device.",
|
||||
7: "A brief description of the device.",
|
||||
8: "Device is enabled when the configuration is loaded.",
|
||||
9: "Device is set to read-only.",
|
||||
10: "This flag allows to configure if the 'trigger' method of the device is called during scans.",
|
||||
}
|
||||
|
||||
# We have at least one device registered
|
||||
def get_device_table_row(column: int):
|
||||
main_app.set_current("device_manager")
|
||||
if dm_widget._initialized is False:
|
||||
return (None, None)
|
||||
table = dm_widget.device_manager_display.device_table_view.table
|
||||
header = table.horizontalHeader()
|
||||
x = header.sectionViewportPosition(column)
|
||||
table.horizontalScrollBar().setValue(x)
|
||||
# Recompute after scrolling
|
||||
x = header.sectionViewportPosition(column)
|
||||
w = header.sectionSize(column)
|
||||
h = header.height()
|
||||
rect = QRect(x, 0, w, h)
|
||||
top_left = header.viewport().mapTo(main_app, rect.topLeft())
|
||||
|
||||
return (QRect(top_left, rect.size()), col_text_mapping.get(column, ""))
|
||||
|
||||
for col, text in col_text_mapping.items():
|
||||
step_id = guided_tour.register_widget(
|
||||
widget=lambda col=col: get_device_table_row(col),
|
||||
title=f"{dm_widget.device_manager_display.device_table_view.table.horizontalHeaderItem(col).text()}",
|
||||
text=text,
|
||||
)
|
||||
step_ids.append(step_id)
|
||||
|
||||
if not step_ids:
|
||||
return None
|
||||
|
||||
return ViewTourSteps(view_title="Device Manager", step_ids=step_ids)
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel
|
||||
from qtpy.QtCore import QEventLoop, Qt, QTimer
|
||||
from qtpy.QtCore import QEventLoop
|
||||
from qtpy.QtWidgets import (
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
@@ -23,42 +23,6 @@ from bec_widgets.widgets.control.device_input.signal_combobox.signal_combobox im
|
||||
from bec_widgets.widgets.plots.waveform.waveform import Waveform
|
||||
|
||||
|
||||
def set_splitter_weights(splitter: QSplitter, weights: List[float]) -> None:
|
||||
"""
|
||||
Apply initial sizes to a splitter using weight ratios, e.g. [1,3,2,1].
|
||||
Works for horizontal or vertical splitters and sets matching stretch factors.
|
||||
"""
|
||||
|
||||
def apply():
|
||||
n = splitter.count()
|
||||
if n == 0:
|
||||
return
|
||||
w = list(weights[:n]) + [1] * max(0, n - len(weights))
|
||||
w = [max(0.0, float(x)) for x in w]
|
||||
tot_w = sum(w)
|
||||
if tot_w <= 0:
|
||||
w = [1.0] * n
|
||||
tot_w = float(n)
|
||||
total_px = (
|
||||
splitter.width()
|
||||
if splitter.orientation() == Qt.Orientation.Horizontal
|
||||
else splitter.height()
|
||||
)
|
||||
if total_px < 2:
|
||||
QTimer.singleShot(0, apply)
|
||||
return
|
||||
sizes = [max(1, int(total_px * (wi / tot_w))) for wi in w]
|
||||
diff = total_px - sum(sizes)
|
||||
if diff != 0:
|
||||
idx = max(range(n), key=lambda i: w[i])
|
||||
sizes[idx] = max(1, sizes[idx] + diff)
|
||||
splitter.setSizes(sizes)
|
||||
for i, wi in enumerate(w):
|
||||
splitter.setStretchFactor(i, max(1, int(round(wi * 100))))
|
||||
|
||||
QTimer.singleShot(0, apply)
|
||||
|
||||
|
||||
class ViewTourSteps(BaseModel):
|
||||
"""Model representing tour steps for a view.
|
||||
|
||||
@@ -68,7 +32,7 @@ class ViewTourSteps(BaseModel):
|
||||
"""
|
||||
|
||||
view_title: str
|
||||
step_ids: list[str]
|
||||
step_ids: List[str]
|
||||
|
||||
|
||||
class ViewBase(QWidget):
|
||||
@@ -142,68 +106,6 @@ class ViewBase(QWidget):
|
||||
"""
|
||||
return None
|
||||
|
||||
####### Default view has to be done with setting up splitters ########
|
||||
def set_default_view(self, horizontal_weights: list, vertical_weights: list):
|
||||
"""Apply initial weights to every horizontal and vertical splitter.
|
||||
|
||||
Examples:
|
||||
horizontal_weights = [1, 3, 2, 1]
|
||||
vertical_weights = [3, 7] # top:bottom = 30:70
|
||||
"""
|
||||
splitters_h = []
|
||||
splitters_v = []
|
||||
for splitter in self.findChildren(QSplitter):
|
||||
if splitter.orientation() == Qt.Orientation.Horizontal:
|
||||
splitters_h.append(splitter)
|
||||
elif splitter.orientation() == Qt.Orientation.Vertical:
|
||||
splitters_v.append(splitter)
|
||||
|
||||
def apply_all():
|
||||
for s in splitters_h:
|
||||
set_splitter_weights(s, horizontal_weights)
|
||||
for s in splitters_v:
|
||||
set_splitter_weights(s, vertical_weights)
|
||||
|
||||
QTimer.singleShot(0, apply_all)
|
||||
|
||||
def set_stretch(self, *, horizontal=None, vertical=None):
|
||||
"""Update splitter weights and re-apply to all splitters.
|
||||
|
||||
Accepts either a list/tuple of weights (e.g., [1,3,2,1]) or a role dict
|
||||
for convenience: horizontal roles = {"left","center","right"},
|
||||
vertical roles = {"top","bottom"}.
|
||||
"""
|
||||
|
||||
def _coerce_h(x):
|
||||
if x is None:
|
||||
return None
|
||||
if isinstance(x, (list, tuple)):
|
||||
return list(map(float, x))
|
||||
if isinstance(x, dict):
|
||||
return [
|
||||
float(x.get("left", 1)),
|
||||
float(x.get("center", x.get("middle", 1))),
|
||||
float(x.get("right", 1)),
|
||||
]
|
||||
return None
|
||||
|
||||
def _coerce_v(x):
|
||||
if x is None:
|
||||
return None
|
||||
if isinstance(x, (list, tuple)):
|
||||
return list(map(float, x))
|
||||
if isinstance(x, dict):
|
||||
return [float(x.get("top", 1)), float(x.get("bottom", 1))]
|
||||
return None
|
||||
|
||||
h = _coerce_h(horizontal)
|
||||
v = _coerce_v(vertical)
|
||||
if h is None:
|
||||
h = [1, 1, 1]
|
||||
if v is None:
|
||||
v = [1, 1]
|
||||
self.set_default_view(h, v)
|
||||
|
||||
|
||||
####################################################################################################
|
||||
# Example views for demonstration/testing purposes
|
||||
|
||||
@@ -45,9 +45,9 @@ class TourStep(TypedDict):
|
||||
widget_ref: (
|
||||
louie.saferef.BoundMethodWeakref
|
||||
| weakref.ReferenceType[
|
||||
QWidget | QAction | Callable[[], tuple[QWidget | QAction, str | None]]
|
||||
QWidget | QAction | QRect | Callable[[], tuple[QWidget | QAction | QRect, str | None]]
|
||||
]
|
||||
| Callable[[], tuple[QWidget | QAction, str | None]]
|
||||
| Callable[[], tuple[QWidget | QAction | QRect, str | None]]
|
||||
| None
|
||||
)
|
||||
text: str
|
||||
@@ -274,7 +274,9 @@ class GuidedTour(QObject):
|
||||
def register_widget(
|
||||
self,
|
||||
*,
|
||||
widget: QWidget | QAction | Callable[[], tuple[QWidget | QAction, str | None]],
|
||||
widget: (
|
||||
QWidget | QAction | QRect | Callable[[], tuple[QWidget | QAction | QRect, str | None]]
|
||||
),
|
||||
text: str = "",
|
||||
title: str = "",
|
||||
) -> str:
|
||||
@@ -282,7 +284,7 @@ class GuidedTour(QObject):
|
||||
Register a widget with help text for tours.
|
||||
|
||||
Args:
|
||||
widget (QWidget | QAction | Callable[[], tuple[QWidget | QAction, str | None]]): The target widget or a callable that returns the widget and its help text.
|
||||
widget (QWidget | QAction | QRect | Callable[[], tuple[QWidget | QAction | QRect, str | None]]): The target widget or a callable that returns the widget and its help text.
|
||||
text (str): The help text for the widget. This will be shown during the tour.
|
||||
title (str, optional): A title for the widget (defaults to its class name or action text).
|
||||
|
||||
@@ -305,6 +307,9 @@ class GuidedTour(QObject):
|
||||
|
||||
widget_ref = _resolve_toolbar_button
|
||||
default_title = getattr(widget, "tooltip", "Toolbar Menu")
|
||||
elif isinstance(widget, QRect):
|
||||
widget_ref = saferef.safe_ref(widget)
|
||||
default_title = "Area"
|
||||
else:
|
||||
widget_ref = saferef.safe_ref(widget)
|
||||
default_title = widget.__class__.__name__ if hasattr(widget, "__class__") else "Widget"
|
||||
@@ -515,7 +520,7 @@ class GuidedTour(QObject):
|
||||
|
||||
self.step_changed.emit(self._current_index + 1, len(self._tour_steps))
|
||||
|
||||
def _resolve_step_target(self, step: TourStep) -> tuple[QWidget | QAction | None, str]:
|
||||
def _resolve_step_target(self, step: TourStep) -> tuple[QWidget | QAction | QRect | None, str]:
|
||||
"""
|
||||
Resolve the target widget/action for the given step.
|
||||
|
||||
@@ -523,7 +528,7 @@ class GuidedTour(QObject):
|
||||
step(TourStep): The tour step to resolve.
|
||||
|
||||
Returns:
|
||||
tuple[QWidget | QAction | None, str]: The resolved target and the step text.
|
||||
tuple[QWidget | QAction | QRect | None, str]: The resolved target, optional QRect, and the step text.
|
||||
"""
|
||||
widget_ref = step.get("widget_ref")
|
||||
step_text = step.get("text", "")
|
||||
@@ -536,7 +541,7 @@ class GuidedTour(QObject):
|
||||
if target is None:
|
||||
return None, step_text
|
||||
|
||||
if callable(target) and not isinstance(target, (QWidget, QAction)):
|
||||
if callable(target) and not isinstance(target, (QWidget, QAction, QRect)):
|
||||
result = target()
|
||||
if isinstance(result, tuple):
|
||||
target, alt_text = result
|
||||
@@ -550,7 +555,7 @@ class GuidedTour(QObject):
|
||||
def _get_highlight_rect(
|
||||
self,
|
||||
main_window: QWidget,
|
||||
target: QWidget | QAction,
|
||||
target: QWidget | QAction | QRect,
|
||||
step_title: str,
|
||||
direction: Literal["next"] | Literal["prev"] = "next",
|
||||
) -> QRect | None:
|
||||
@@ -565,6 +570,8 @@ class GuidedTour(QObject):
|
||||
Returns:
|
||||
QRect | None: The rectangle to highlight, or None if not found/visible.
|
||||
"""
|
||||
if isinstance(target, QRect):
|
||||
return target
|
||||
if isinstance(target, QAction):
|
||||
rect = self._action_highlight_rect(target)
|
||||
if rect is None:
|
||||
@@ -588,7 +595,12 @@ class GuidedTour(QObject):
|
||||
return QRect(top_left, rect.size())
|
||||
|
||||
if isinstance(target, QTableWidgetItem):
|
||||
# NOTE: On header items (which are also QTableWidgetItems), this does not work,
|
||||
# Header items are just used as data containers by Qt, thus, we have to directly
|
||||
# pass the QRect through the method (+ make sure the appropriate header section
|
||||
# is visible). This can be handled in the callable method.)
|
||||
table = target.tableWidget()
|
||||
|
||||
if self._visible_check:
|
||||
if not table.isVisible():
|
||||
self._advance_past_invalid_step(
|
||||
@@ -597,13 +609,16 @@ class GuidedTour(QObject):
|
||||
direction=direction,
|
||||
)
|
||||
return None
|
||||
table.scrollToItem(target, QAbstractItemView.ScrollHint.PositionAtCenter)
|
||||
rect = table.visualItemRect(target)
|
||||
top_left = table.viewport().mapTo(main_window, rect.topLeft())
|
||||
return QRect(top_left, rect.size())
|
||||
|
||||
# Table item
|
||||
if table.item(target.row(), target.column()) == target:
|
||||
table.scrollToItem(target, QAbstractItemView.ScrollHint.PositionAtCenter)
|
||||
rect = table.visualItemRect(target)
|
||||
top_left = table.viewport().mapTo(main_window, rect.topLeft())
|
||||
return QRect(top_left, rect.size())
|
||||
|
||||
self._advance_past_invalid_step(
|
||||
step_title, reason=f"Unsupported step target type: {type(target)}"
|
||||
step_title, reason=f"Unsupported step target type: {type(target)}", direction=direction
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
||||
from qtpy.QtCore import QRect
|
||||
from qtpy.QtWidgets import QAction, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.guided_tour import GuidedTour
|
||||
from bec_widgets.utils.toolbars.actions import ExpandableMenuAction, MaterialIconAction
|
||||
@@ -40,6 +41,10 @@ class DummyWidget(QWidget):
|
||||
return True
|
||||
|
||||
|
||||
class DummyAction(QAction):
|
||||
"""A dummy action for testing purposes."""
|
||||
|
||||
|
||||
class TestGuidedTour:
|
||||
"""Test the GuidedTour class core functionality."""
|
||||
|
||||
@@ -403,3 +408,73 @@ class TestGuidedTour:
|
||||
guided_help.start_tour()
|
||||
guided_help.overlay.paintEvent(None) # Force paint event to render text
|
||||
qtbot.wait(300) # Wait for rendering
|
||||
|
||||
def test_advanced_past_invalid_tour_step(
|
||||
self, guided_help: GuidedTour, test_widget: QWidget, qtbot
|
||||
):
|
||||
"""Test that an invalid tour step is handled gracefully."""
|
||||
widget_id_valid = guided_help.register_widget(
|
||||
widget=test_widget, text="Test widget for overlay", title="OverlayWidget"
|
||||
)
|
||||
widget_id_invalid = guided_help.register_widget(
|
||||
widget=lambda: None, text="Test2", title="something"
|
||||
)
|
||||
widget_id_valid2 = guided_help.register_widget(
|
||||
widget=test_widget, text="Test3", title="something"
|
||||
)
|
||||
|
||||
widget_valid = guided_help._registered_widgets[widget_id_valid]["widget_ref"]()
|
||||
widget_valid2 = guided_help._registered_widgets[widget_id_valid2]["widget_ref"]()
|
||||
|
||||
with (
|
||||
mock.patch.object(widget_valid, "isVisible", return_value=True),
|
||||
mock.patch.object(widget_valid2, "isVisible", return_value=True),
|
||||
):
|
||||
guided_help.create_tour(
|
||||
[widget_id_valid, widget_id_invalid, widget_id_valid2, "nonexistent_id"]
|
||||
)
|
||||
|
||||
with qtbot.waitSignal(guided_help.tour_started, timeout=2000) as blocker:
|
||||
guided_help.start_tour()
|
||||
|
||||
assert blocker.signal_triggered
|
||||
with qtbot.waitSignal(guided_help.step_changed) as step_blocker:
|
||||
guided_help.next_step() # Move to step 2 (invalid, should skip)
|
||||
assert step_blocker.signal_triggered
|
||||
assert step_blocker.args == [3, 3]
|
||||
|
||||
with qtbot.waitSignal(guided_help.step_changed) as step_blocker:
|
||||
guided_help.prev_step() # Move back to step 1
|
||||
assert step_blocker.signal_triggered
|
||||
assert step_blocker.args == [1, 3]
|
||||
|
||||
def test_get_highlight_rect(self, guided_help: GuidedTour, test_widget: QWidget, qtbot):
|
||||
"""Test that _get_highlight_rect returns a QRect for a valid widget."""
|
||||
widget = DummyWidget(test_widget) # Use a dummy widget that is always visible
|
||||
action = DummyAction(test_widget)
|
||||
test_rect = QRect(10, 10, 100, 50)
|
||||
|
||||
with mock.patch.object(widget, "isVisible", return_value=True):
|
||||
rect = guided_help._get_highlight_rect(guided_help.main_window, widget, "Test step")
|
||||
|
||||
assert isinstance(rect, QRect)
|
||||
|
||||
rect = guided_help._get_highlight_rect(guided_help.main_window, test_rect, "Test step")
|
||||
|
||||
assert isinstance(rect, QRect)
|
||||
|
||||
# QAction should not be available, thus skipped
|
||||
|
||||
with mock.patch.object(guided_help, "_advance_past_invalid_step") as mock_advance:
|
||||
rect = guided_help._get_highlight_rect(
|
||||
guided_help.main_window, action, "Test step", direction="next"
|
||||
)
|
||||
assert rect is None
|
||||
mock_advance.assert_called_once()
|
||||
|
||||
mock_advance.reset_mock()
|
||||
rect = guided_help._get_highlight_rect(
|
||||
guided_help.main_window, action, "Test step", direction="prev"
|
||||
)
|
||||
assert rect is None
|
||||
mock_advance.assert_called_once()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import pytest
|
||||
from qtpy.QtCore import QRect
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.applications.main_app import BECMainApp
|
||||
@@ -46,6 +47,9 @@ class SpyVetoView(SpyView):
|
||||
def app_with_spies(qtbot, mocked_client):
|
||||
app = BECMainApp(client=mocked_client, anim_duration=ANIM_TEST_DURATION, show_examples=False)
|
||||
qtbot.addWidget(app)
|
||||
# App must be shown properly to ensure visibility checks work
|
||||
# Call .show() and then waitExposed
|
||||
app.show()
|
||||
qtbot.waitExposed(app)
|
||||
|
||||
app.add_section("Tests", id="tests")
|
||||
@@ -163,14 +167,54 @@ def test_views_can_extend_guided_tour(app_with_spies):
|
||||
assert hasattr(ide_tour, "step_ids")
|
||||
assert isinstance(ide_tour.step_ids, list)
|
||||
|
||||
# Get all registered widgets
|
||||
widgets = app.guided_tour.get_registered_widgets()
|
||||
|
||||
# pylint: disable=protected-access
|
||||
# Test that ide_tour has valid steps and targets
|
||||
for step_id in ide_tour.step_ids:
|
||||
assert step_id in widgets
|
||||
tour_step = widgets.get(step_id)
|
||||
target, text = app.guided_tour._resolve_step_target(tour_step)
|
||||
assert isinstance(text, str)
|
||||
assert text != ""
|
||||
if target is not None: # If step should be skipped
|
||||
highlighted_rect = app.guided_tour._get_highlight_rect(app, target, tour_step["title"])
|
||||
if (
|
||||
highlighted_rect is not None
|
||||
): # If widget is not visible, it will be skipped and return None
|
||||
assert isinstance(highlighted_rect, QRect)
|
||||
|
||||
# Test that dm_tour has valid steps and targets, test it once
|
||||
# with _initialized = True and False. This leads to different tour paths.
|
||||
for init in [False, True]:
|
||||
app.device_manager.device_manager_widget._initialized = init
|
||||
for step_id in dm_tour.step_ids:
|
||||
assert step_id in widgets
|
||||
tour_step = widgets.get(step_id)
|
||||
target, text = app.guided_tour._resolve_step_target(tour_step)
|
||||
assert isinstance(text, str)
|
||||
assert text != ""
|
||||
if target is not None: # If step should be skipped
|
||||
highlighted_rect = app.guided_tour._get_highlight_rect(
|
||||
app, target, tour_step["title"]
|
||||
)
|
||||
if (
|
||||
highlighted_rect is not None
|
||||
): # If widget is not visible, it will be skipped and return None
|
||||
assert isinstance(highlighted_rect, QRect)
|
||||
|
||||
|
||||
def test_guided_tour_can_start_and_stop(app_with_spies, qtbot):
|
||||
"""Test that the guided tour can be started and stopped."""
|
||||
app, _, _, _ = app_with_spies
|
||||
|
||||
app: BECMainApp
|
||||
|
||||
# Start the tour
|
||||
app.start_guided_tour()
|
||||
qtbot.wait(100)
|
||||
with qtbot.waitSignal(app.guided_tour.tour_started, timeout=2000) as blocker:
|
||||
app.start_guided_tour()
|
||||
assert blocker.signal_triggered
|
||||
|
||||
# Check that tour is active
|
||||
assert app.guided_tour._active
|
||||
@@ -178,8 +222,10 @@ def test_guided_tour_can_start_and_stop(app_with_spies, qtbot):
|
||||
assert app.guided_tour.overlay.isVisible()
|
||||
|
||||
# Stop the tour
|
||||
app.guided_tour.stop_tour()
|
||||
qtbot.wait(100)
|
||||
with qtbot.waitSignal(app.guided_tour.tour_finished, timeout=2000) as blocker:
|
||||
app.guided_tour.stop_tour()
|
||||
|
||||
assert blocker.signal_triggered
|
||||
|
||||
# Check that tour is stopped
|
||||
assert not app.guided_tour._active
|
||||
|
||||
Reference in New Issue
Block a user