mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-14 03:31:50 +02:00
294 lines
9.9 KiB
Python
294 lines
9.9 KiB
Python
import webbrowser
|
||
|
||
import pytest
|
||
from qtpy.QtCore import QEvent, QPoint, QPointF
|
||
from qtpy.QtGui import QEnterEvent
|
||
from qtpy.QtWidgets import QApplication, QFrame, QLabel
|
||
|
||
from bec_widgets.widgets.containers.main_window.addons.hover_widget import (
|
||
HoverWidget,
|
||
WidgetTooltip,
|
||
)
|
||
from bec_widgets.widgets.containers.main_window.addons.scroll_label import ScrollLabel
|
||
from bec_widgets.widgets.containers.main_window.addons.web_links import BECWebLinksMixin
|
||
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow
|
||
|
||
from .client_mocks import mocked_client
|
||
from .conftest import create_widget
|
||
|
||
|
||
@pytest.fixture
|
||
def bec_main_window(qtbot, mocked_client):
|
||
widget = BECMainWindow(client=mocked_client)
|
||
qtbot.addWidget(widget)
|
||
qtbot.waitExposed(widget)
|
||
yield widget
|
||
|
||
|
||
#################################################################
|
||
# Tests for BECMainWindow Initialization and Functionality
|
||
#################################################################
|
||
|
||
|
||
def test_bec_main_window_initialization(bec_main_window):
|
||
assert isinstance(bec_main_window, BECMainWindow)
|
||
assert bec_main_window.windowTitle() == "BEC"
|
||
assert bec_main_window.app is not None
|
||
assert bec_main_window.statusBar() is not None
|
||
assert bec_main_window._app_id_label is not None
|
||
|
||
|
||
def test_bec_main_window_display_client_message(qtbot, bec_main_window):
|
||
"""
|
||
Verify that display_client_message updates the client‑info label.
|
||
"""
|
||
test_msg = "Client connected successfully"
|
||
bec_main_window.display_client_message({"message": test_msg}, {})
|
||
qtbot.wait(200)
|
||
assert bec_main_window._client_info_label.text() == test_msg
|
||
|
||
|
||
def test_status_bar_has_separator(bec_main_window):
|
||
"""Ensure the status bar contains at least one vertical separator."""
|
||
status_bar = bec_main_window.statusBar()
|
||
separators = [w for w in status_bar.findChildren(QFrame) if w.frameShape() == QFrame.VLine]
|
||
assert separators, "Expected at least one QFrame separator in the status bar."
|
||
|
||
|
||
#################################################################
|
||
# Tests for BECMainWindow Addons
|
||
#################################################################
|
||
|
||
#################################################################
|
||
# Tests for ScrollLabel behaviour
|
||
|
||
|
||
def test_scroll_label_does_not_scroll_when_text_fits(qtbot):
|
||
"""Label with short text should not activate scrolling timer."""
|
||
lbl = create_widget(qtbot, ScrollLabel) # shorten delay for test speed
|
||
qtbot.addWidget(lbl)
|
||
lbl.resize(200, 20)
|
||
lbl.setText("Short text")
|
||
# Process events to allow timer logic to run
|
||
qtbot.wait(200)
|
||
assert not lbl._timer.isActive()
|
||
assert not lbl._delay_timer.isActive()
|
||
|
||
|
||
def test_scroll_label_starts_scrolling(qtbot):
|
||
"""Label with long text should start _delay_timer; later _timer becomes active."""
|
||
lbl = create_widget(qtbot, ScrollLabel, delay_ms=100)
|
||
lbl.resize(150, 20)
|
||
long_text = "This is a very long piece of text that should definitely overflow the label width"
|
||
lbl.setText(long_text)
|
||
# Immediately after setText, only delay‑timer should be active
|
||
assert lbl._delay_timer.isActive()
|
||
assert not lbl._timer.isActive()
|
||
# Wait until scrolling timer becomes active
|
||
qtbot.waitUntil(lambda: lbl._timer.isActive(), timeout=2000)
|
||
assert lbl._timer.isActive()
|
||
|
||
|
||
def test_scroll_label_scroll_method(qtbot):
|
||
"""Directly exercise _scroll to ensure offset advances and paintEvent is invoked."""
|
||
lbl = create_widget(qtbot, ScrollLabel, step_px=5) # shorten delay for test speed
|
||
qtbot.addWidget(lbl)
|
||
lbl.resize(120, 20)
|
||
lbl.setText("x" * 200) # long text to guarantee overflow
|
||
qtbot.wait(200) # let timers configure themselves
|
||
|
||
# Capture current offset and force a manual scroll tick
|
||
old_offset = lbl._offset
|
||
lbl._scroll()
|
||
assert lbl._offset == old_offset + 5
|
||
|
||
|
||
def test_scroll_label_paint_event(qtbot):
|
||
"""
|
||
Grab the widget as a pixmap; this calls paintEvent under the hood
|
||
and ensures no exceptions occur during rendering.
|
||
"""
|
||
lbl = create_widget(qtbot, ScrollLabel) # shorten delay for test speed
|
||
qtbot.addWidget(lbl)
|
||
lbl.resize(180, 20)
|
||
lbl.setText("Rendering check")
|
||
lbl.show()
|
||
qtbot.wait(200) # allow Qt to schedule a paint
|
||
pixmap = lbl.grab()
|
||
assert not pixmap.isNull()
|
||
|
||
|
||
def test_display_client_message_with_expiration(qtbot, bec_main_window):
|
||
"""
|
||
A message with a finite 'expire' value should disappear once the timer
|
||
fires.
|
||
"""
|
||
test_msg = "This message should vanish fast"
|
||
expire_sec = 0.2
|
||
|
||
bec_main_window.display_client_message({"message": test_msg, "expire": expire_sec}, {})
|
||
|
||
assert bec_main_window._client_info_expire_timer.isActive()
|
||
assert bec_main_window._client_info_label.text() == test_msg
|
||
|
||
qtbot.waitUntil(lambda: not bec_main_window._client_info_expire_timer.isActive(), timeout=1000)
|
||
|
||
assert bec_main_window._client_info_label.text() == ""
|
||
|
||
|
||
def test_display_client_message_no_expiration(qtbot, bec_main_window):
|
||
"""
|
||
A message with 'expire' == 0 must persist and never start the timer.
|
||
"""
|
||
test_msg = "Persistent status message"
|
||
|
||
bec_main_window.display_client_message({"message": test_msg, "expire": 0}, {})
|
||
|
||
assert not bec_main_window._client_info_expire_timer.isActive()
|
||
assert bec_main_window._client_info_label.text() == test_msg
|
||
|
||
qtbot.wait(500)
|
||
assert bec_main_window._client_info_label.text() == test_msg
|
||
|
||
|
||
def test_display_client_message_overwrite_resets_timer(qtbot, bec_main_window):
|
||
"""
|
||
Sending a second message while the expiration timer is active should
|
||
overwrite the first and stop the timer if the second one is persistent.
|
||
"""
|
||
first_msg = "First (temporary)"
|
||
second_msg = "Second (persistent)"
|
||
|
||
bec_main_window.display_client_message({"message": first_msg, "expire": 0.3}, {})
|
||
qtbot.wait(200)
|
||
assert bec_main_window._client_info_expire_timer.isActive()
|
||
|
||
bec_main_window.display_client_message({"message": second_msg, "expire": 0}, {})
|
||
|
||
assert not bec_main_window._client_info_expire_timer.isActive()
|
||
assert bec_main_window._client_info_label.text() == second_msg
|
||
|
||
qtbot.wait(400)
|
||
assert bec_main_window._client_info_label.text() == second_msg
|
||
|
||
|
||
#################################################################
|
||
# Tests for BECWebLinksMixin (webbrowser opening)
|
||
|
||
|
||
def test_bec_weblinks(monkeypatch):
|
||
opened_urls = []
|
||
|
||
def fake_open(url):
|
||
opened_urls.append(url)
|
||
|
||
monkeypatch.setattr(webbrowser, "open", fake_open)
|
||
|
||
BECWebLinksMixin.open_bec_docs()
|
||
BECWebLinksMixin.open_bec_widgets_docs()
|
||
BECWebLinksMixin.open_bec_bug_report()
|
||
|
||
assert opened_urls == [
|
||
"https://beamline-experiment-control.readthedocs.io/en/latest/",
|
||
"https://bec.readthedocs.io/projects/bec-widgets/en/latest/",
|
||
"https://gitlab.psi.ch/groups/bec/-/issues/",
|
||
]
|
||
|
||
|
||
#################################################################
|
||
# Tests for scan‑progress bar animations
|
||
|
||
|
||
def test_scan_progress_bar_show_animation(qtbot, bec_main_window):
|
||
"""
|
||
_show_scan_progress_bar should animate the container's maximumWidth
|
||
from 0 to the configured target width.
|
||
"""
|
||
container = bec_main_window._scan_progress_bar_with_separator
|
||
|
||
# Pre‑condition: collapsed
|
||
assert container.maximumWidth() == 0
|
||
|
||
bec_main_window._show_scan_progress_bar()
|
||
|
||
target = bec_main_window._scan_progress_bar_target_width
|
||
qtbot.waitUntil(lambda: container.maximumWidth() == target, timeout=2000)
|
||
|
||
assert container.maximumWidth() == target
|
||
|
||
|
||
def test_scan_progress_bar_hide_animation(qtbot, bec_main_window):
|
||
"""
|
||
_animate_hide_scan_progress_bar should collapse the container back to 0 width.
|
||
"""
|
||
container = bec_main_window._scan_progress_bar_with_separator
|
||
|
||
# First expand it
|
||
bec_main_window._show_scan_progress_bar()
|
||
target = bec_main_window._scan_progress_bar_target_width
|
||
qtbot.waitUntil(lambda: container.maximumWidth() == target, timeout=2000)
|
||
|
||
# Trigger hide animation
|
||
bec_main_window._animate_hide_scan_progress_bar()
|
||
|
||
qtbot.waitUntil(lambda: container.maximumWidth() == 0, timeout=2000)
|
||
|
||
assert container.maximumWidth() == 0
|
||
|
||
|
||
#################################################################
|
||
# Tests for hover widget and tooltip behaviour
|
||
|
||
|
||
def test_hover_widget_tooltip(qtbot):
|
||
"""
|
||
After a HoverWidget is closed, its WidgetTooltip must be gone.
|
||
"""
|
||
simple = QLabel("Hover me")
|
||
full = QLabel("Full details")
|
||
hover = create_widget(qtbot, HoverWidget, simple=simple, full=full)
|
||
|
||
assert hover._simple is simple
|
||
assert hover._full is full
|
||
assert hover._tooltip is None
|
||
|
||
|
||
def test_widget_tooltip_show_and_hide(qtbot):
|
||
"""
|
||
WidgetTooltip should appear when show_above is called and hide on Leave.
|
||
"""
|
||
full_lbl = QLabel("Standalone tooltip content")
|
||
tooltip = create_widget(qtbot, WidgetTooltip, content=full_lbl)
|
||
|
||
# Show above an arbitrary point
|
||
pos = QPoint(200, 200)
|
||
tooltip.show_above(pos)
|
||
assert tooltip.isVisible()
|
||
|
||
# Send a synthetic Leave event
|
||
QApplication.sendEvent(tooltip, QEvent(QEvent.Leave))
|
||
qtbot.waitUntil(lambda: not tooltip.isVisible(), timeout=500)
|
||
assert not tooltip.isVisible()
|
||
|
||
|
||
def test_hover_widget_mouse_events(qtbot):
|
||
"""
|
||
Verify that HoverWidget responds correctly to Enter, MouseMove, and Leave
|
||
events, keeping the tooltip visible only while the pointer is inside.
|
||
"""
|
||
simple = QLabel("Hover‑target")
|
||
full = QLabel("Full‑view")
|
||
hover = create_widget(qtbot, HoverWidget, simple=simple, full=full)
|
||
|
||
local = QPointF(hover.rect().center()) # inside widget
|
||
scene = QPointF(hover.mapTo(hover.window(), local.toPoint()))
|
||
global_ = QPointF(hover.mapToGlobal(local.toPoint()))
|
||
|
||
enter_event = QEnterEvent(local, scene, global_)
|
||
hover.enterEvent(event=enter_event)
|
||
qtbot.wait(200)
|
||
|
||
assert hover._tooltip is not None
|
||
assert hover._tooltip.isVisible()
|
||
assert hover._tooltip.content is full
|