Skip to content

Commit 5ec1f74

Browse files
committed
feat: add comprehensive tests for callback system and widget interactions
1 parent 9b864b3 commit 5ec1f74

2 files changed

Lines changed: 27 additions & 131 deletions

File tree

anyplotlib/tests/test_interactive/test_events.py renamed to anyplotlib/tests/test_interactive/test_callbacks.py

Lines changed: 19 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
"""
2-
tests/test_events.py
3-
====================
2+
tests/test_interactive/test_callbacks.py
3+
========================================
44
55
Tests for the unified object-level callback system.
66
7+
Covers:
78
* Event dataclass – event_type / source / data / attribute forwarding
89
* CallbackRegistry – connect / disconnect / fire (event_type dispatch only)
910
* Plot2D / Plot1D / PlotMesh / Plot3D – on_changed / on_release / on_click
10-
* Widget-level – @wid.on_changed / @wid.on_release / @wid.on_click
1111
* Figure._on_event – JSON routing to widget + plot callbacks
1212
* Practical patterns
13-
* Interactive FFT example – unit tests (pure Python, no browser)
13+
14+
Widget-level callback and event-dispatch integration tests live in
15+
``test_widgets.py``.
1416
"""
1517

1618
from __future__ import annotations
@@ -457,65 +459,34 @@ def cb(event): fired.append(event)
457459

458460

459461
# ─────────────────────────────────────────────────────────────────────────────
460-
# 7. Widget-level callbacks (@wid.on_changed / on_release / on_click)
462+
# 7. Figure._on_event routing
461463
# ─────────────────────────────────────────────────────────────────────────────
462464

463-
class TestWidgetLevelCallbacks:
465+
class TestFigureEventRouting:
464466

465-
def test_on_changed_fires_on_drag_frame(self):
467+
def test_dispatch_reaches_plot_callbacks(self):
466468
fig, ax = apl.subplots(1, 1)
467469
v = ax.imshow(np.zeros((32, 32)))
468-
wid = v.add_widget("circle")
469470
fired = []
470471

471-
@wid.on_changed
472+
@v.on_release
472473
def cb(event): fired.append(event)
473474

474-
_simulate_js_event(fig, v, "on_changed", widget_id=wid, cx=10.0, cy=20.0)
475+
_simulate_js_event(fig, v, "on_release", cx=10.0, cy=20.0)
475476
assert len(fired) == 1
476477
assert fired[0].cx == pytest.approx(10.0)
477-
assert fired[0].source is wid
478-
479-
def test_on_release_fires_on_mouseup(self):
480-
fig, ax = apl.subplots(1, 1)
481-
v = ax.imshow(np.zeros((32, 32)))
482-
wid = v.add_widget("rectangle")
483-
fired = []
484-
485-
@wid.on_release
486-
def cb(event): fired.append(event)
487-
488-
_simulate_js_event(fig, v, "on_release", widget_id=wid,
489-
x=5.0, y=5.0, w=20.0, h=20.0)
490-
assert len(fired) == 1
491-
assert fired[0].event_type == "on_release"
492-
493-
def test_on_click_fires_without_state_change(self):
494-
"""on_click must fire even when no field values changed."""
495-
fig, ax = apl.subplots(1, 1)
496-
v = ax.imshow(np.zeros((32, 32)))
497-
wid = v.add_widget("crosshair", cx=16.0, cy=16.0)
498-
fired = []
499-
500-
@wid.on_click
501-
def cb(event): fired.append(event)
502478

503-
_simulate_js_event(fig, v, "on_click", widget_id=wid, cx=16.0, cy=16.0)
504-
assert len(fired) == 1
505-
506-
def test_on_changed_not_fire_for_release(self):
479+
def test_dispatch_with_widget_id_updates_widget(self):
507480
fig, ax = apl.subplots(1, 1)
508481
v = ax.imshow(np.zeros((32, 32)))
509-
wid = v.add_widget("circle")
510-
fired = []
511-
512-
@wid.on_changed
513-
def cb(event): fired.append(event)
482+
wid = v.add_widget("circle", cx=0.0, cy=0.0)
514483

515-
_simulate_js_event(fig, v, "on_release", widget_id=wid, cx=5.0, cy=5.0)
516-
assert fired == []
484+
_simulate_js_event(fig, v, "on_changed", widget_id=wid, cx=5.0)
485+
assert wid.cx == pytest.approx(5.0)
517486

518-
def test_widget_and_plot_both_fire(self):
487+
def test_widget_and_plot_callbacks_both_fire(self):
488+
"""A single JS event bearing a widget_id fires both the widget-level
489+
and the plot-level on_release callbacks, with the widget as source."""
519490
fig, ax = apl.subplots(1, 1)
520491
v = ax.imshow(np.zeros((32, 32)))
521492
wid = v.add_widget("circle")
@@ -532,84 +503,6 @@ def pc(event): p_fired.append(event)
532503
assert w_fired[0].source is wid
533504
assert p_fired[0].source is wid
534505

535-
def test_widget_state_updated_after_js_event(self):
536-
fig, ax = apl.subplots(1, 1)
537-
v = ax.imshow(np.zeros((32, 32)))
538-
wid = v.add_widget("rectangle", x=0.0, y=0.0, w=10.0, h=10.0)
539-
540-
_simulate_js_event(fig, v, "on_changed", widget_id=wid,
541-
x=50.0, y=60.0, w=20.0, h=20.0)
542-
assert wid.x == pytest.approx(50.0)
543-
assert wid.y == pytest.approx(60.0)
544-
545-
def test_no_echo_from_python_push(self):
546-
fig, ax = apl.subplots(1, 1)
547-
v = ax.imshow(np.zeros((32, 32)))
548-
wid = v.add_widget("circle")
549-
fired = []
550-
551-
@wid.on_changed
552-
def cb(event): fired.append(event)
553-
554-
fig._on_event({"new": json.dumps({
555-
"source": "python", "panel_id": v._id,
556-
"widget_id": wid._id, "cx": 99.0
557-
})})
558-
assert fired == []
559-
560-
def test_1d_vline_widget_event(self):
561-
fig, ax = apl.subplots(1, 1)
562-
v = ax.plot(np.zeros(64))
563-
wid = v.add_vline_widget(x=10.0)
564-
fired = []
565-
566-
@wid.on_changed
567-
def cb(event): fired.append(event)
568-
569-
_simulate_js_event(fig, v, "on_changed", widget_id=wid, x=30.0)
570-
assert len(fired) == 1
571-
assert fired[0].x == pytest.approx(30.0)
572-
573-
def test_1d_range_widget_event(self):
574-
fig, ax = apl.subplots(1, 1)
575-
v = ax.plot(np.zeros(64))
576-
wid = v.add_range_widget(x0=5.0, x1=15.0)
577-
fired = []
578-
579-
@wid.on_release
580-
def cb(event): fired.append(event)
581-
582-
_simulate_js_event(fig, v, "on_release", widget_id=wid, x0=8.0, x1=20.0)
583-
assert len(fired) == 1
584-
assert fired[0].x0 == pytest.approx(8.0)
585-
586-
587-
# ─────────────────────────────────────────────────────────────────────────────
588-
# 8. Figure._on_event routing
589-
# ─────────────────────────────────────────────────────────────────────────────
590-
591-
class TestFigureOnEvent:
592-
593-
def test_dispatch_reaches_plot_callbacks(self):
594-
fig, ax = apl.subplots(1, 1)
595-
v = ax.imshow(np.zeros((32, 32)))
596-
fired = []
597-
598-
@v.on_release
599-
def cb(event): fired.append(event)
600-
601-
_simulate_js_event(fig, v, "on_release", cx=10.0, cy=20.0)
602-
assert len(fired) == 1
603-
assert fired[0].cx == pytest.approx(10.0)
604-
605-
def test_dispatch_with_widget_id_updates_widget(self):
606-
fig, ax = apl.subplots(1, 1)
607-
v = ax.imshow(np.zeros((32, 32)))
608-
wid = v.add_widget("circle", cx=0.0, cy=0.0)
609-
610-
_simulate_js_event(fig, v, "on_changed", widget_id=wid, cx=5.0)
611-
assert wid.cx == pytest.approx(5.0)
612-
613506
def test_dispatch_wrong_panel_id_ignored(self):
614507
fig, ax = apl.subplots(1, 1)
615508
v = ax.imshow(np.zeros((32, 32)))
@@ -690,7 +583,7 @@ def cb(event): fired.append(event)
690583

691584

692585
# ─────────────────────────────────────────────────────────────────────────────
693-
# 9. Practical patterns
586+
# 8. Practical patterns
694587
# ─────────────────────────────────────────────────────────────────────────────
695588

696589
class TestPracticalPatterns:

‎anyplotlib/tests/test_interactive/test_widgets.py‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
"""
2-
tests/test_widgets.py
3-
=====================
2+
tests/test_interactive/test_widgets.py
3+
=======================================
44
55
Tests for the Widget class system and the event_json dispatch pipeline.
66
77
Covers:
88
* Widget creation, attribute access, set(), to_dict(), __setattr__
99
* on_changed / on_release / on_click decorator + disconnect
1010
* _update_from_js — always fires for on_release/on_click
11-
* Plot2D / Plot1D widget integration
11+
* Widget visibility — hide() / show()
12+
* Plot2D / Plot1D widget integration (add / remove / list / clear)
1213
* Figure event_json dispatch (JS→Python path via _simulate_js_event)
13-
* widget.x = 40 attribute assignment
14-
* widget.x read-back after JS event
1514
* End-to-end FFT example with simulated JS drag
15+
* Interactive fitting scenario (PointWidget + RangeWidget + line.on_click)
16+
17+
Callback infrastructure (Event, CallbackRegistry, plot-level callbacks,
18+
Figure routing) is tested in ``test_callbacks.py``.
1619
"""
1720

1821
from __future__ import annotations

0 commit comments

Comments
 (0)