Skip to content

Commit 4338bb7

Browse files
Copilotwyzula-jan
authored andcommitted
test(ring_progress_bar): add unit tests for hover behavior
1 parent f1a0c02 commit 4338bb7

1 file changed

Lines changed: 208 additions & 5 deletions

File tree

tests/unit_tests/test_ring_progress_bar.py

Lines changed: 208 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
import json
44

55
import pytest
6-
from bec_lib.endpoints import MessageEndpoints
7-
from pydantic import ValidationError
6+
from qtpy.QtCore import QPoint, QPointF
87
from qtpy.QtGui import QColor
98

109
from bec_widgets.utils import Colors
11-
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_bar import RingProgressBar
10+
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_bar import (
11+
RingProgressBar,
12+
RingProgressContainerWidget,
13+
)
1214

1315
from .client_mocks import mocked_client
1416

@@ -432,8 +434,6 @@ def test_gap_affects_ring_positioning(ring_progress_bar):
432434
for _ in range(3):
433435
ring_progress_bar.add_ring()
434436

435-
initial_gap = ring_progress_bar.gap
436-
437437
# Change gap
438438
new_gap = 30
439439
ring_progress_bar.set_gap(new_gap)
@@ -467,3 +467,206 @@ def test_rings_property_returns_correct_list(ring_progress_bar):
467467
# Should return the same list
468468
assert rings_via_property is rings_direct
469469
assert len(rings_via_property) == 3
470+
471+
472+
###################################
473+
# Hover behavior tests
474+
###################################
475+
476+
477+
@pytest.fixture
478+
def container(qtbot):
479+
widget = RingProgressContainerWidget()
480+
qtbot.addWidget(widget)
481+
widget.resize(200, 200)
482+
yield widget
483+
484+
485+
def _ring_center_pos(container):
486+
"""Return (center_x, center_y, base_radius) for a square container."""
487+
size = min(container.width(), container.height())
488+
center_x = container.width() / 2
489+
center_y = container.height() / 2
490+
max_ring_size = container.get_max_ring_size()
491+
base_radius = (size - 2 * max_ring_size) / 2
492+
return center_x, center_y, base_radius
493+
494+
495+
def test_ring_at_pos_no_rings(container):
496+
assert container._ring_at_pos(QPointF(100, 100)) is None
497+
498+
499+
def test_ring_at_pos_center_is_inside_rings(container):
500+
"""The center of the widget is inside all rings; _ring_at_pos should return None."""
501+
container.add_ring()
502+
assert container._ring_at_pos(QPointF(100, 100)) is None
503+
504+
505+
def test_ring_at_pos_on_single_ring(container):
506+
"""A point on the ring arc should resolve to that ring."""
507+
ring = container.add_ring()
508+
cx, cy, base_radius = _ring_center_pos(container)
509+
# Point exactly on the ring centerline
510+
pos = QPointF(cx + base_radius, cy)
511+
assert container._ring_at_pos(pos) is ring
512+
513+
514+
def test_ring_at_pos_outside_all_rings(container):
515+
"""A point well outside the outermost ring returns None."""
516+
container.add_ring()
517+
cx, cy, base_radius = _ring_center_pos(container)
518+
line_width = container.rings[0].config.line_width
519+
# Place point clearly beyond the outer edge
520+
pos = QPointF(cx + base_radius + line_width + 5, cy)
521+
assert container._ring_at_pos(pos) is None
522+
523+
524+
def test_ring_at_pos_selects_correct_ring_from_multiple(qtbot):
525+
"""With multiple rings, each position resolves to the right ring."""
526+
container = RingProgressContainerWidget()
527+
qtbot.addWidget(container)
528+
container.resize(300, 300)
529+
530+
ring0 = container.add_ring()
531+
ring1 = container.add_ring()
532+
533+
size = min(container.width(), container.height())
534+
cx = container.width() / 2
535+
cy = container.height() / 2
536+
max_ring_size = container.get_max_ring_size()
537+
base_radius = (size - 2 * max_ring_size) / 2
538+
539+
radius0 = base_radius - ring0.gap
540+
radius1 = base_radius - ring1.gap
541+
542+
assert container._ring_at_pos(QPointF(cx + radius0, cy)) is ring0
543+
assert container._ring_at_pos(QPointF(cx + radius1, cy)) is ring1
544+
545+
546+
def test_set_hovered_ring_sets_flag(container):
547+
"""_set_hovered_ring marks the ring as hovered and updates _hovered_ring."""
548+
ring = container.add_ring()
549+
assert container._hovered_ring is None
550+
container._set_hovered_ring(ring)
551+
assert container._hovered_ring is ring
552+
assert ring._hovered is True
553+
554+
555+
def test_set_hovered_ring_to_none_clears_flag(container):
556+
"""Calling _set_hovered_ring(None) un-hovers the current ring."""
557+
ring = container.add_ring()
558+
container._set_hovered_ring(ring)
559+
container._set_hovered_ring(None)
560+
assert container._hovered_ring is None
561+
assert ring._hovered is False
562+
563+
564+
def test_set_hovered_ring_switches_between_rings(qtbot):
565+
"""Switching hover from one ring to another correctly updates both flags."""
566+
container = RingProgressContainerWidget()
567+
qtbot.addWidget(container)
568+
ring0 = container.add_ring()
569+
ring1 = container.add_ring()
570+
571+
container._set_hovered_ring(ring0)
572+
assert ring0._hovered is True
573+
assert ring1._hovered is False
574+
575+
container._set_hovered_ring(ring1)
576+
assert ring0._hovered is False
577+
assert ring1._hovered is True
578+
assert container._hovered_ring is ring1
579+
580+
581+
def test_build_tooltip_text_manual_mode(container):
582+
"""Manual mode tooltip contains mode label, value, max and percentage."""
583+
ring = container.add_ring()
584+
ring.set_value(50)
585+
ring.set_min_max_values(0, 100)
586+
587+
text = RingProgressContainerWidget._build_tooltip_text(ring)
588+
assert "Manual" in text
589+
assert "50.0%" in text
590+
assert "100" in text
591+
592+
593+
def test_build_tooltip_text_scan_mode(container):
594+
"""Scan mode tooltip labels the mode as 'Scan progress'."""
595+
ring = container.add_ring()
596+
ring.config.mode = "scan"
597+
ring.set_value(25)
598+
599+
text = RingProgressContainerWidget._build_tooltip_text(ring)
600+
assert "Scan progress" in text
601+
602+
603+
def test_build_tooltip_text_device_mode_with_signal(container):
604+
"""Device mode tooltip shows device:signal when both are set."""
605+
ring = container.add_ring()
606+
ring.config.mode = "device"
607+
ring.config.device = "samx"
608+
ring.config.signal = "readback"
609+
ring.set_value(10)
610+
611+
text = RingProgressContainerWidget._build_tooltip_text(ring)
612+
assert "Device" in text
613+
assert "samx:readback" in text
614+
615+
616+
def test_build_tooltip_text_device_mode_without_signal(container):
617+
"""Device mode tooltip shows only device name when signal is absent."""
618+
ring = container.add_ring()
619+
ring.config.mode = "device"
620+
ring.config.device = "samy"
621+
ring.config.signal = None
622+
ring.set_value(10)
623+
624+
text = RingProgressContainerWidget._build_tooltip_text(ring)
625+
assert "samy" in text
626+
assert ":" not in text.split("Device:")[-1].split("\n")[0]
627+
628+
629+
def test_build_tooltip_text_nonzero_min_shows_range(container):
630+
"""Tooltip includes Range line when min_value is not 0."""
631+
ring = container.add_ring()
632+
ring.set_min_max_values(10, 90)
633+
ring.set_value(50)
634+
635+
text = RingProgressContainerWidget._build_tooltip_text(ring)
636+
assert "Range" in text
637+
638+
639+
def test_build_tooltip_text_zero_min_no_range(container):
640+
"""Tooltip omits Range line when min_value is 0."""
641+
ring = container.add_ring()
642+
ring.set_min_max_values(0, 100)
643+
ring.set_value(50)
644+
645+
text = RingProgressContainerWidget._build_tooltip_text(ring)
646+
assert "Range" not in text
647+
648+
649+
def test_refresh_hover_tooltip_updates_label_on_value_change(container):
650+
"""refresh_hover_tooltip updates the label text after the ring value changes."""
651+
ring = container.add_ring()
652+
ring.set_value(30)
653+
container._hovered_ring = ring
654+
container._last_hover_global_pos = QPoint(100, 100)
655+
656+
container.refresh_hover_tooltip(ring)
657+
text_before = container._hover_tooltip_label.text()
658+
659+
ring.set_value(70)
660+
container.refresh_hover_tooltip(ring)
661+
text_after = container._hover_tooltip_label.text()
662+
663+
assert text_before != text_after
664+
assert "70" in text_after
665+
666+
667+
def test_refresh_hover_tooltip_no_pos_does_not_crash(container):
668+
"""refresh_hover_tooltip with no stored position should return without raising."""
669+
ring = container.add_ring()
670+
container._last_hover_global_pos = None
671+
# Should not raise
672+
container.refresh_hover_tooltip(ring)

0 commit comments

Comments
 (0)