From dd89a24834a4cfa34b7e6f8e5b2f6c0fed5b441d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Althviz=20Mor=C3=A9?= Date: Wed, 21 Jun 2023 20:08:39 -0500 Subject: [PATCH] Add tests/more tests to widgets with low coverage (#5864) # Description * [x] Add more tests for `QColorSwatch` related widgets * [x] Add test for `QtToolTipLabel` * [x] Add test for `QtPluginSorter` ## Notes While checking the addition of new tests to widgets using the [code coverage as guidance](https://app.codecov.io/gh/napari/napari/tree/main/napari/_qt/widgets), seems like a widget which doesn't have coverage at all exist: `QtDictTable`. But more interestingly, seems like this widget is not used anymore? I was unable to find a place inside napari source code where this widget is used. Maybe it should be removed or moved to an upstream project like `superqt`? --- .../widgets/_tests/test_qt_color_swatch.py | 21 +++ .../widgets/_tests/test_qt_plugin_sorter.py | 147 ++++++++++++++++++ napari/_qt/widgets/_tests/test_qt_tooltip.py | 23 +++ 3 files changed, 191 insertions(+) create mode 100644 napari/_qt/widgets/_tests/test_qt_plugin_sorter.py create mode 100644 napari/_qt/widgets/_tests/test_qt_tooltip.py diff --git a/napari/_qt/widgets/_tests/test_qt_color_swatch.py b/napari/_qt/widgets/_tests/test_qt_color_swatch.py index 23d78e85296..238d7e0a5f0 100644 --- a/napari/_qt/widgets/_tests/test_qt_color_swatch.py +++ b/napari/_qt/widgets/_tests/test_qt_color_swatch.py @@ -1,8 +1,11 @@ import numpy as np import pytest +from qtpy.QtCore import Qt +from qtpy.QtWidgets import QApplication from napari._qt.widgets.qt_color_swatch import ( TRANSPARENT, + QColorPopup, QColorSwatch, QColorSwatchEdit, ) @@ -17,9 +20,18 @@ def test_succesfull_create_qcolorswatchedit(qtbot, color, tooltip): test_color = color or TRANSPARENT test_tooltip = tooltip or 'click to set color' + # check widget creation and base values assert widget.color_swatch.toolTip() == test_tooltip np.testing.assert_array_equal(widget.color, test_color) + # check widget popup + qtbot.mouseRelease(widget.color_swatch, Qt.MouseButton.LeftButton) + color_popup = None + for widget in QApplication.topLevelWidgets(): + if isinstance(widget, QColorPopup): + color_popup = widget + assert color_popup + @pytest.mark.parametrize('color', [None, [1, 1, 1, 1]]) @pytest.mark.parametrize('tooltip', [None, 'This is a test']) @@ -30,5 +42,14 @@ def test_succesfull_create_qcolorswatch(qtbot, color, tooltip): test_color = color or TRANSPARENT test_tooltip = tooltip or 'click to set color' + # check widget creation and base values assert widget.toolTip() == test_tooltip np.testing.assert_array_equal(widget.color, test_color) + + # check widget popup + qtbot.mouseRelease(widget, Qt.MouseButton.LeftButton) + color_popup = None + for widget in QApplication.topLevelWidgets(): + if isinstance(widget, QColorPopup): + color_popup = widget + assert color_popup diff --git a/napari/_qt/widgets/_tests/test_qt_plugin_sorter.py b/napari/_qt/widgets/_tests/test_qt_plugin_sorter.py new file mode 100644 index 00000000000..34d4b3caa73 --- /dev/null +++ b/napari/_qt/widgets/_tests/test_qt_plugin_sorter.py @@ -0,0 +1,147 @@ +import pytest + +from napari._qt.widgets.qt_plugin_sorter import QtPluginSorter, rst2html + + +@pytest.mark.parametrize( + 'text,expected_text', + [ + ("", ""), + ( + """Return a function capable of loading ``path`` into napari, or ``None``. + + This is the primary "**reader plugin**" function. It accepts a path or + list of paths, and returns a list of data to be added to the ``Viewer``. + The function may return ``[(None, )]`` to indicate that the file was read + successfully, but did not contain any data. + + The main place this hook is used is in :func:`Viewer.open() + `, via the + :func:`~napari.plugins.io.read_data_with_plugins` function. + + It will also be called on ``File -> Open...`` or when a user drops a file + or folder onto the viewer. This function must execute **quickly**, and + should return ``None`` if the filepath is of an unrecognized format for + this reader plugin. If ``path`` is determined to be recognized format, + this function should return a *new* function that accepts the same filepath + (or list of paths), and returns a list of ``LayerData`` tuples, where each + tuple is a 1-, 2-, or 3-tuple of ``(data,)``, ``(data, meta)``, or ``(data, + meta, layer_type)``. + + ``napari`` will then use each tuple in the returned list to generate a new + layer in the viewer using the :func:`Viewer._add_layer_from_data() + ` + method. The first, (optional) second, and (optional) third items in each + tuple in the returned layer_data list, therefore correspond to the + ``data``, ``meta``, and ``layer_type`` arguments of the + :func:`Viewer._add_layer_from_data() + ` + method, respectively. + + .. important:: + + ``path`` may be either a ``str`` or a ``list`` of ``str``. If a + ``list``, then each path in the list can be assumed to be one part of a + larger multi-dimensional stack (for instance: a list of 2D image files + that should be stacked along a third axis). Implementations should do + their own checking for ``list`` or ``str``, and handle each case as + desired.""", + 'Return a function capable of loading path into napari, or None.

' + 'This is the primary "reader plugin" function. It accepts a path or
' + 'list of paths, and returns a list of data to be added to the Viewer.
' + 'The function may return [(None, )] to indicate that the file was read
' + 'successfully, but did not contain any data.

' + 'The main place this hook is used is in Viewer.open(), via the
' + 'read_data_with_plugins function.

' + 'It will also be called on File -> Open... or when a user drops a file
' + 'or folder onto the viewer. This function must execute quickly, and
' + 'should return None if the filepath is of an unrecognized format for
' + 'this reader plugin. If path is determined to be recognized format,
' + 'this function should return a new function that accepts the same filepath
' + '(or list of paths), and returns a list of LayerData tuples, where each
' + 'tuple is a 1-, 2-, or 3-tuple of (data,), (data, meta), or (data,
' + 'meta, layer_type)
.

napari will then use each tuple in the returned list to generate a new
' + 'layer in the viewer using the Viewer._add_layer_from_data()
' + 'method. The first, (optional) second, and (optional) third items in each
' + 'tuple in the returned layer_data list, therefore correspond to the
' + 'data, meta, and layer_type arguments of the
' + 'Viewer._add_layer_from_data()
method, respectively.

.. important::

' + ' path may be either a str or a list of str. If a
' + ' list, then each path in the list can be assumed to be one part of a
' + 'larger multi-dimensional stack (for instance: a list of 2D image files
' + 'that should be stacked along a third axis). Implementations should do
' + 'their own checking for list or str, and handle each case as
' + 'desired.', + ), + ], +) +def test_rst2html(text, expected_text): + assert rst2html(text) == expected_text + + +def test_create_qt_plugin_sorter(qtbot): + plugin_sorter = QtPluginSorter() + qtbot.addWidget(plugin_sorter) + + # Check initial hook combobox items + hook_combo_box = plugin_sorter.hook_combo_box + combobox_items = [ + hook_combo_box.itemText(idx) for idx in range(hook_combo_box.count()) + ] + assert combobox_items == [ + 'select hook... ', + 'get_reader', + 'get_writer', + 'write_image', + 'write_labels', + 'write_points', + 'write_shapes', + 'write_surface', + 'write_vectors', + ] + + +@pytest.mark.parametrize( + "hook_name,help_info", + [ + ('select hook... ', ''), + ( + 'get_reader', + 'This is the primary "reader plugin" function. It accepts a path or
list of paths, and returns a list of data to be added to the Viewer.
', + ), + ( + 'get_writer', + 'This function will be called whenever the user attempts to save multiple
layers (e.g. via File -> Save Layers, or
save_layers).
', + ), + ( + 'write_labels', + 'It is the responsibility of the implementation to check any extension on
path and return None if it is an unsupported extension.', + ), + ( + 'write_points', + 'It is the responsibility of the implementation to check any extension on
path and return None if it is an unsupported extension.', + ), + ( + 'write_shapes', + 'It is the responsibility of the implementation to check any extension on
path and return None if it is an unsupported extension.', + ), + ( + 'write_surface', + 'It is the responsibility of the implementation to check any extension on
path and return None if it is an unsupported extension.', + ), + ( + 'write_vectors', + 'It is the responsibility of the implementation to check any extension on
path and return None if it is an unsupported extension.', + ), + ], +) +def test_qt_plugin_sorter_help_info(qtbot, hook_name, help_info): + plugin_sorter = QtPluginSorter() + qtbot.addWidget(plugin_sorter) + + # Check hook combobox items help tooltip in the info widget + info_widget = plugin_sorter.info + hook_combo_box = plugin_sorter.hook_combo_box + hook_combo_box.setCurrentText(hook_name) + + assert help_info in info_widget.toolTip() diff --git a/napari/_qt/widgets/_tests/test_qt_tooltip.py b/napari/_qt/widgets/_tests/test_qt_tooltip.py new file mode 100644 index 00000000000..821a5b4c233 --- /dev/null +++ b/napari/_qt/widgets/_tests/test_qt_tooltip.py @@ -0,0 +1,23 @@ +import sys + +import pytest +from qtpy.QtWidgets import QToolTip + +from napari._qt.widgets.qt_tooltip import QtToolTipLabel + + +@pytest.mark.skipif( + sys.platform.startswith('linux') or sys.platform == 'darwin', + reason='Timeouts when running on CI with Linux or macOS', +) +def test_qt_tooltip_label(qtbot): + tooltip_text = "Test QtToolTipLabel showing a tooltip" + widget = QtToolTipLabel("Label with a tooltip") + widget.setToolTip(tooltip_text) + qtbot.addWidget(widget) + widget.show() + + assert QToolTip.text() == "" + qtbot.mouseMove(widget) + qtbot.waitUntil(lambda: QToolTip.isVisible()) + qtbot.waitUntil(lambda: QToolTip.text() == tooltip_text)