From 7f757a936dc4eb862179420f16180c0fe987a6ca Mon Sep 17 00:00:00 2001 From: Sahil Kumar Singh <60318530+SahilKumar75@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:08:30 +0530 Subject: [PATCH 1/2] Use headless viewer for layer style tests --- tests/fixtures/napari.py | 7 +++++++ .../test_napari_plugin/test_layer_styles.py | 14 ++++++-------- .../test_napari_plugin/test_layer_wiring.py | 11 ----------- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/tests/fixtures/napari.py b/tests/fixtures/napari.py index 5a2c8e5047..110a42ffd5 100644 --- a/tests/fixtures/napari.py +++ b/tests/fixtures/napari.py @@ -4,6 +4,7 @@ import numpy as np import pandas as pd import pytest +from napari.components import ViewerModel from napari.layers.base import ActionType from movement.io import save_poses @@ -11,6 +12,12 @@ from movement.napari.loader_widgets import POINTS_LAYER_KEY, DataLoader +@pytest.fixture +def headless_napari_viewer(): + """Return a headless napari viewer model.""" + return ViewerModel() + + @pytest.fixture def valid_poses_path_and_ds(valid_poses_dataset, tmp_path): """Return a (path, dataset) pair representing a poses dataset diff --git a/tests/test_unit/test_napari_plugin/test_layer_styles.py b/tests/test_unit/test_napari_plugin/test_layer_styles.py index 2be8815f95..91f4f1b677 100644 --- a/tests/test_unit/test_napari_plugin/test_layer_styles.py +++ b/tests/test_unit/test_napari_plugin/test_layer_styles.py @@ -346,11 +346,9 @@ def test_regions_style_colors(color, expected_rgb): @pytest.mark.parametrize("n_shapes", [1, 3]) -def test_regions_style_set_color_all_shapes( - make_napari_viewer_proxy, n_shapes -): +def test_regions_style_set_color_all_shapes(headless_napari_viewer, n_shapes): """Test that set_color_all_shapes applies colors to all shapes.""" - viewer = make_napari_viewer_proxy() + viewer = headless_napari_viewer # Create shapes data (rectangles) shapes_data = [ @@ -380,10 +378,10 @@ def test_regions_style_set_color_all_shapes( def test_regions_style_set_color_all_shapes_empty_layer( - make_napari_viewer_proxy, + headless_napari_viewer, ): """Test that set_color_all_shapes handles empty layers gracefully.""" - viewer = make_napari_viewer_proxy() + viewer = headless_napari_viewer layer = viewer.add_shapes() # Assert it's an empty layer assert len(layer.data) == 0 @@ -404,12 +402,12 @@ def test_regions_style_set_color_all_shapes_empty_layer( ], ) def test_regions_style_set_style_for_new_shapes( - make_napari_viewer_proxy, selected_data + headless_napari_viewer, selected_data ): """Test that set_style_for_new_shapes runs without error, regardless of whether the layer is selected or not. """ - viewer = make_napari_viewer_proxy() + viewer = headless_napari_viewer # Create a shape shapes_data = [[[0, 0], [0, 10], [10, 10], [10, 0]]] diff --git a/tests/test_unit/test_napari_plugin/test_layer_wiring.py b/tests/test_unit/test_napari_plugin/test_layer_wiring.py index 3d80ff10b7..303dbec152 100644 --- a/tests/test_unit/test_napari_plugin/test_layer_wiring.py +++ b/tests/test_unit/test_napari_plugin/test_layer_wiring.py @@ -11,7 +11,6 @@ import numpy as np import pytest -from napari.components import ViewerModel from napari.components.dims import RangeTuple from napari.layers.base import ActionType @@ -24,16 +23,6 @@ from movement.napari.meta_widget import MovementMetaWidget -@pytest.fixture -def headless_napari_viewer(): - """Return a headless napari viewer model. - - Faster than make_napari_viewer_proxy because it does not - require Qt viewer construction and teardown. - """ - return ViewerModel() - - @pytest.fixture def orphan_viewer_and_layers(valid_poses_path_and_ds, loaded_data_loader): """Return viewer and layers of a loaded dataset whose widget is gc-ed.""" From 6008f68186c987430f037557ce225018316a9069 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Singh <60318530+SahilKumar75@users.noreply.github.com> Date: Thu, 8 Oct 2026 19:29:03 +0530 Subject: [PATCH 2/2] Use headless viewers for save widget unit tests --- .../test_napari_plugin/test_layer_wiring.py | 4 +- .../test_napari_plugin/test_save_widget.py | 39 +++++++++---------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/tests/test_unit/test_napari_plugin/test_layer_wiring.py b/tests/test_unit/test_napari_plugin/test_layer_wiring.py index 303dbec152..c940727e25 100644 --- a/tests/test_unit/test_napari_plugin/test_layer_wiring.py +++ b/tests/test_unit/test_napari_plugin/test_layer_wiring.py @@ -200,11 +200,11 @@ def test_layer_wiring_survives_closing_metawidget( def test_connect_viewer_callbacks_twice_does_not_duplicate( - make_napari_viewer_proxy, + headless_napari_viewer, ): """Test that wiring a viewer twice does not duplicate the callbacks.""" # Wire the viewer callbacks once - viewer = make_napari_viewer_proxy() + viewer = headless_napari_viewer connect_viewer_callbacks(viewer) # Count callbacks linked to the viewer, for each of the four events diff --git a/tests/test_unit/test_napari_plugin/test_save_widget.py b/tests/test_unit/test_napari_plugin/test_save_widget.py index 169d4849f4..e5c38b6014 100644 --- a/tests/test_unit/test_napari_plugin/test_save_widget.py +++ b/tests/test_unit/test_napari_plugin/test_save_widget.py @@ -18,10 +18,16 @@ ) -def test_data_saver_widget_instantiation(make_napari_viewer_proxy): - """Test that the save widget is properly instantiated.""" - data_saver_widget = DataSaver(make_napari_viewer_proxy()) +@pytest.fixture +def data_saver_widget(headless_napari_viewer, qtbot): + """Return a real Qt save widget without constructing a viewer window.""" + widget = DataSaver(headless_napari_viewer) + qtbot.addWidget(widget) + return widget + +def test_data_saver_widget_instantiation(data_saver_widget): + """Test that the save widget is properly instantiated.""" assert data_saver_widget.layout().rowCount() == 1 assert isinstance(data_saver_widget.save_button, QPushButton) assert data_saver_widget.save_button.objectName() == "save_button" @@ -29,12 +35,11 @@ def test_data_saver_widget_instantiation(make_napari_viewer_proxy): assert data_saver_widget.save_button.toolTip() == DISABLED_TOOLTIP -def test_save_button_enabled_for_valid_points_layer(make_napari_viewer_proxy): +def test_save_button_enabled_for_valid_points_layer(data_saver_widget): """Test that selecting a valid movement points layer enables the save button and updates its tooltip. """ - viewer = make_napari_viewer_proxy() - data_saver_widget = DataSaver(viewer) + viewer = data_saver_widget.viewer layer = viewer.add_points( name="points", @@ -70,9 +75,7 @@ def test_save_button_enabled_for_valid_points_layer(make_napari_viewer_proxy): ), ], ) -def test_save_button_disabled_for_invalid_layer( - make_layer, make_napari_viewer_proxy -): +def test_save_button_disabled_for_invalid_layer(make_layer, data_saver_widget): """Test that selecting a layer that is not a movement points layer keeps the save button disabled with the default tooltip. @@ -80,8 +83,7 @@ def test_save_button_disabled_for_invalid_layer( treating the mere presence of ``POINTS_PROPERTIES_KEY`` (even if falsy/None) as a stand-in for "this layer was created by movement". """ - viewer = make_napari_viewer_proxy() - data_saver_widget = DataSaver(viewer) + viewer = data_saver_widget.viewer viewer.layers.selection.active = make_layer(viewer) @@ -89,7 +91,7 @@ def test_save_button_disabled_for_invalid_layer( assert data_saver_widget.save_button.toolTip() == DISABLED_TOOLTIP -def test_disabled_button_click_does_not_save(make_napari_viewer_proxy, mocker): +def test_disabled_button_click_does_not_save(data_saver_widget, mocker): """Test that clicking the disabled save button (no valid layer selected) never opens the file dialog. @@ -97,8 +99,7 @@ def test_disabled_button_click_does_not_save(make_napari_viewer_proxy, mocker): sole gate on saving, so ``_on_save_clicked`` can safely assume the active layer is a valid movement points layer. """ - viewer = make_napari_viewer_proxy() - data_saver_widget = DataSaver(viewer) + viewer = data_saver_widget.viewer viewer.layers.selection.active = viewer.add_image( np.zeros((10, 10)), name="an image" @@ -114,10 +115,9 @@ def test_disabled_button_click_does_not_save(make_napari_viewer_proxy, mocker): mock_file_dialog.assert_not_called() -def test_save_clicked_cancelled_dialog(make_napari_viewer_proxy, mocker): +def test_save_clicked_cancelled_dialog(data_saver_widget, mocker): """Test that cancelling the file dialog does not attempt to save.""" - viewer = make_napari_viewer_proxy() - data_saver_widget = DataSaver(viewer) + viewer = data_saver_widget.viewer layer = viewer.add_points( name="points", @@ -212,12 +212,11 @@ def test_save_failure_shows_error( assert not out_path.exists() -def test_close_event_disconnects_selection_signal(make_napari_viewer_proxy): +def test_close_event_disconnects_selection_signal(data_saver_widget): """Test that closing the widget disconnects the layer selection callback, so it no longer reacts to further selection changes. """ - viewer = make_napari_viewer_proxy() - data_saver_widget = DataSaver(viewer) + viewer = data_saver_widget.viewer data_saver_widget.closeEvent(QCloseEvent())