Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions tests/fixtures/napari.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@
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
from movement.napari.layer_wiring import on_points_data_changed
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
Expand Down
14 changes: 6 additions & 8 deletions tests/test_unit/test_napari_plugin/test_layer_styles.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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
Expand All @@ -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]]]
Expand Down
15 changes: 2 additions & 13 deletions tests/test_unit/test_napari_plugin/test_layer_wiring.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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."""
Expand Down Expand Up @@ -211,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
Expand Down
39 changes: 19 additions & 20 deletions tests/test_unit/test_napari_plugin/test_save_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,28 @@
)


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"
assert not data_saver_widget.save_button.isEnabled()
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",
Expand Down Expand Up @@ -70,35 +75,31 @@ 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.

The ``points_with_properties_but_no_layer_key`` case guards against
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)

assert not data_saver_widget.save_button.isEnabled()
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.

This guards the invariant that the button's enabled state is the
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"
Expand All @@ -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",
Expand Down Expand Up @@ -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())

Expand Down