1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-05-01 20:42:30 +02:00

test(ring_progress_bar): add unit tests for hover behavior

This commit is contained in:
copilot-swe-agent[bot]
2026-03-17 13:13:14 +00:00
committed by Jan Wyzula
parent 2f75aaea16
commit 6e5f6e7fbb
2 changed files with 310 additions and 6 deletions
+280 -6
View File
@@ -1,14 +1,18 @@
# pylint: disable=missing-function-docstring, missing-module-docstring, unused-import
import json
from unittest.mock import MagicMock
import pytest
from bec_lib.endpoints import MessageEndpoints
from pydantic import ValidationError
from qtpy.QtGui import QColor
from qtpy.QtCore import QEvent, QPoint, QPointF, Qt
from qtpy.QtGui import QColor, QMouseEvent
from qtpy.QtWidgets import QApplication
from bec_widgets.utils import Colors
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_bar import RingProgressBar
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_bar import (
RingProgressBar,
RingProgressContainerWidget,
)
from .client_mocks import mocked_client
@@ -432,8 +436,6 @@ def test_gap_affects_ring_positioning(ring_progress_bar):
for _ in range(3):
ring_progress_bar.add_ring()
initial_gap = ring_progress_bar.gap
# Change gap
new_gap = 30
ring_progress_bar.set_gap(new_gap)
@@ -467,3 +469,275 @@ def test_rings_property_returns_correct_list(ring_progress_bar):
# Should return the same list
assert rings_via_property is rings_direct
assert len(rings_via_property) == 3
###################################
# Hover behavior tests
###################################
@pytest.fixture
def container(qtbot):
widget = RingProgressContainerWidget()
qtbot.addWidget(widget)
widget.resize(200, 200)
yield widget
def _ring_center_pos(container):
"""Return (center_x, center_y, base_radius) for a square container."""
size = min(container.width(), container.height())
center_x = container.width() / 2
center_y = container.height() / 2
max_ring_size = container.get_max_ring_size()
base_radius = (size - 2 * max_ring_size) / 2
return center_x, center_y, base_radius
def _send_mouse_move(widget, pos: QPoint):
global_pos = widget.mapToGlobal(pos)
event = QMouseEvent(
QEvent.Type.MouseMove,
QPointF(pos),
QPointF(global_pos),
Qt.MouseButton.NoButton,
Qt.MouseButton.NoButton,
Qt.KeyboardModifier.NoModifier,
)
QApplication.sendEvent(widget, event)
def test_ring_at_pos_no_rings(container):
assert container._ring_at_pos(QPointF(100, 100)) is None
def test_ring_at_pos_center_is_inside_rings(container):
"""The center of the widget is inside all rings; _ring_at_pos should return None."""
container.add_ring()
assert container._ring_at_pos(QPointF(100, 100)) is None
def test_ring_at_pos_on_single_ring(container):
"""A point on the ring arc should resolve to that ring."""
ring = container.add_ring()
cx, cy, base_radius = _ring_center_pos(container)
# Point exactly on the ring centerline
pos = QPointF(cx + base_radius, cy)
assert container._ring_at_pos(pos) is ring
def test_ring_at_pos_outside_all_rings(container):
"""A point well outside the outermost ring returns None."""
container.add_ring()
cx, cy, base_radius = _ring_center_pos(container)
line_width = container.rings[0].config.line_width
# Place point clearly beyond the outer edge
pos = QPointF(cx + base_radius + line_width + 5, cy)
assert container._ring_at_pos(pos) is None
def test_ring_at_pos_selects_correct_ring_from_multiple(qtbot):
"""With multiple rings, each position resolves to the right ring."""
container = RingProgressContainerWidget()
qtbot.addWidget(container)
container.resize(300, 300)
ring0 = container.add_ring()
ring1 = container.add_ring()
size = min(container.width(), container.height())
cx = container.width() / 2
cy = container.height() / 2
max_ring_size = container.get_max_ring_size()
base_radius = (size - 2 * max_ring_size) / 2
radius0 = base_radius - ring0.gap
radius1 = base_radius - ring1.gap
assert container._ring_at_pos(QPointF(cx + radius0, cy)) is ring0
assert container._ring_at_pos(QPointF(cx + radius1, cy)) is ring1
def test_set_hovered_ring_sets_flag(container):
"""_set_hovered_ring marks the ring as hovered and updates _hovered_ring."""
ring = container.add_ring()
assert container._hovered_ring is None
container._set_hovered_ring(ring)
assert container._hovered_ring is ring
assert ring._hovered is True
def test_set_hovered_ring_to_none_clears_flag(container):
"""Calling _set_hovered_ring(None) un-hovers the current ring."""
ring = container.add_ring()
container._set_hovered_ring(ring)
container._set_hovered_ring(None)
assert container._hovered_ring is None
assert ring._hovered is False
def test_set_hovered_ring_switches_between_rings(qtbot):
"""Switching hover from one ring to another correctly updates both flags."""
container = RingProgressContainerWidget()
qtbot.addWidget(container)
ring0 = container.add_ring()
ring1 = container.add_ring()
container._set_hovered_ring(ring0)
assert ring0._hovered is True
assert ring1._hovered is False
container._set_hovered_ring(ring1)
assert ring0._hovered is False
assert ring1._hovered is True
assert container._hovered_ring is ring1
def test_build_tooltip_text_manual_mode(container):
"""Manual mode tooltip contains mode label, value, max and percentage."""
ring = container.add_ring()
ring.set_value(50)
ring.set_min_max_values(0, 100)
text = RingProgressContainerWidget._build_tooltip_text(ring)
assert "Manual" in text
assert "50.0%" in text
assert "100" in text
def test_build_tooltip_text_scan_mode(container):
"""Scan mode tooltip labels the mode as 'Scan progress'."""
ring = container.add_ring()
ring.config.mode = "scan"
ring.set_value(25)
text = RingProgressContainerWidget._build_tooltip_text(ring)
assert "Scan progress" in text
def test_build_tooltip_text_device_mode_with_signal(container):
"""Device mode tooltip shows device:signal when both are set."""
ring = container.add_ring()
ring.config.mode = "device"
ring.config.device = "samx"
ring.config.signal = "readback"
ring.set_value(10)
text = RingProgressContainerWidget._build_tooltip_text(ring)
assert "Device" in text
assert "samx:readback" in text
def test_build_tooltip_text_device_mode_without_signal(container):
"""Device mode tooltip shows only device name when signal is absent."""
ring = container.add_ring()
ring.config.mode = "device"
ring.config.device = "samy"
ring.config.signal = None
ring.set_value(10)
text = RingProgressContainerWidget._build_tooltip_text(ring)
assert "samy" in text
assert ":" not in text.split("Device:")[-1].split("\n")[0]
def test_build_tooltip_text_nonzero_min_shows_range(container):
"""Tooltip includes Range line when min_value is not 0."""
ring = container.add_ring()
ring.set_min_max_values(10, 90)
ring.set_value(50)
text = RingProgressContainerWidget._build_tooltip_text(ring)
assert "Range" in text
def test_build_tooltip_text_zero_min_no_range(container):
"""Tooltip omits Range line when min_value is 0."""
ring = container.add_ring()
ring.set_min_max_values(0, 100)
ring.set_value(50)
text = RingProgressContainerWidget._build_tooltip_text(ring)
assert "Range" not in text
def test_refresh_hover_tooltip_updates_label_on_value_change(container):
"""refresh_hover_tooltip updates the label text after the ring value changes."""
ring = container.add_ring()
ring.set_value(30)
container._hovered_ring = ring
container._last_hover_global_pos = QPoint(100, 100)
container.refresh_hover_tooltip(ring)
text_before = container._hover_tooltip_label.text()
ring.set_value(70)
container.refresh_hover_tooltip(ring)
text_after = container._hover_tooltip_label.text()
assert text_before != text_after
assert "70" in text_after
def test_refresh_hover_tooltip_no_pos_does_not_crash(container):
"""refresh_hover_tooltip with no stored position should return without raising."""
ring = container.add_ring()
container._last_hover_global_pos = None
# Should not raise
container.refresh_hover_tooltip(ring)
def test_mouse_move_sets_hovered_ring_and_updates_tooltip(qtbot, container):
ring = container.add_ring()
container._hover_tooltip.show_near = MagicMock()
container.show()
qtbot.waitExposed(container)
cx, cy, base_radius = _ring_center_pos(container)
_send_mouse_move(container, QPoint(int(cx + base_radius), int(cy)))
assert container._hovered_ring is ring
assert ring._hovered is True
assert "Mode: Manual" in container._hover_tooltip_label.text()
container._hover_tooltip.show_near.assert_called_once()
def test_mouse_move_switches_hover_between_rings(qtbot):
container = RingProgressContainerWidget()
qtbot.addWidget(container)
container.resize(300, 300)
ring0 = container.add_ring()
ring1 = container.add_ring()
container._hover_tooltip.show_near = MagicMock()
container.show()
qtbot.waitExposed(container)
cx, cy, base_radius = _ring_center_pos(container)
radius0 = base_radius - ring0.gap
radius1 = base_radius - ring1.gap
_send_mouse_move(container, QPoint(int(cx + radius0), int(cy)))
assert container._hovered_ring is ring0
_send_mouse_move(container, QPoint(int(cx + radius1), int(cy)))
assert container._hovered_ring is ring1
assert ring0._hovered is False
assert ring1._hovered is True
def test_leave_event_clears_hover_and_hides_tooltip(qtbot, container):
ring = container.add_ring()
container._hover_tooltip.hide = MagicMock()
container.show()
qtbot.waitExposed(container)
cx, cy, base_radius = _ring_center_pos(container)
_send_mouse_move(container, QPoint(int(cx + base_radius), int(cy)))
QApplication.sendEvent(container, QEvent(QEvent.Type.Leave))
assert container._hovered_ring is None
assert ring._hovered is False
assert container._last_hover_global_pos is None
container._hover_tooltip.hide.assert_called()
@@ -240,6 +240,36 @@ def test_set_start_angle(ring_widget):
assert ring_widget.config.start_position == 180
def test_set_hovered_updates_animation_target(ring_widget):
ring_widget.set_hovered(True)
assert ring_widget._hovered is True
assert ring_widget._hover_animation.endValue() == 1.0
ring_widget.set_hovered(False)
assert ring_widget._hovered is False
assert ring_widget._hover_animation.endValue() == 0.0
def test_refresh_hover_tooltip_delegates_to_container(ring_widget):
ring_widget.progress_container = MagicMock()
ring_widget.progress_container.is_ring_hovered.return_value = True
ring_widget._refresh_hover_tooltip()
ring_widget.progress_container.refresh_hover_tooltip.assert_called_once_with(ring_widget)
def test_refresh_hover_tooltip_skips_when_ring_is_not_hovered(ring_widget):
ring_widget.progress_container = MagicMock()
ring_widget.progress_container.is_ring_hovered.return_value = False
ring_widget._refresh_hover_tooltip()
ring_widget.progress_container.refresh_hover_tooltip.assert_not_called()
###################################
# Color management tests
###################################