Repository: mne-tools/mne-python

Issue:
MAINT: Add pytest-qt
We should use pytest-qt in tests. It does some nice stuff, including ensuring that exceptions during asynchronous event processing are turned into proper exceptions, like:

```
________________________________________________________________________ test_plot_instance_components[qt] ________________________________________________________________________
mne/viz/tests/test_ica.py:449: in test_plot_instance_components
    fig._fake_click((x, y), xform='data')
../mne-qt-browser/mne_qt_browser/_pg_figure.py:4512: in _fake_click
    raise RuntimeError(f'There as been an {exc[0]} inside the Qt '
E   RuntimeError: There as been an <class 'RuntimeWarning'> inside the Qt event loop (look above for traceback).
------------------------------------------------------------------------------ Captured stderr call -------------------------------------------------------------------------------
Exceptions caught in Qt event loop:
________________________________________________________________________________
Traceback (most recent call last):
  File "/home/larsoner/python/virtualenvs/base/lib/python3.11/site-packages/pyqtgraph/GraphicsScene/GraphicsScene.py", line 378, in sendClickEvent
    item.mouseClickEvent(ev)
  File "/home/larsoner/python/mne-qt-browser/mne_qt_browser/_pg_figure.py", line 605, in mouseClickEvent
    y_values = np.asarray(list(self.ch_texts.values()))[:, 1, :]
               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^
IndexError: too many indices for array: array is 1-dimensional, but 3 were indexed

During handling of the above exception, another exception occurred:

...
  File "/home/larsoner/python/mne-python/mne/viz/tests/test_ica.py", line 449, in test_plot_instance_components
    fig._fake_click((x, y), xform='data')
  File "/home/larsoner/python/mne-qt-browser/mne_qt_browser/_pg_figure.py", line 4502, in _fake_click
    _mouseClick(widget=widget, pos=point, button=button)
  File "/home/larsoner/python/mne-qt-browser/mne_qt_browser/_pg_figure.py", line 4672, in _mouseClick
    _mouseRelease(widget, pos, button, modifier)
  File "/home/larsoner/python/mne-qt-browser/mne_qt_browser/_pg_figure.py", line 4656, in _mouseRelease
    QApplication.sendEvent(widget, event)
  File "/home/larsoner/python/virtualenvs/base/lib/python3.11/site-packages/pyqtgraph/widgets/GraphicsView.py", line 343, in mouseReleaseEvent
...
    super().mouseReleaseEvent(ev)
  File "/home/larsoner/python/mne-qt-browser/mne_qt_browser/_pg_figure.py", line 605, in mouseClickEvent
    y_values = np.asarray(list(self.ch_texts.values()))[:, 1, :]
               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^
IndexError: too many indices for array: array is 1-dimensional, but 3 were indexed
```
I'll probably add this to mne-qt-browser first if it's not there already, then add it here.


Discussion:

Resolving patch:
diff --git a/mne/conftest.py b/mne/conftest.py
--- a/mne/conftest.py
+++ b/mne/conftest.py
@@ -209,6 +209,8 @@ def verbose_debug():
 def qt_config():
     """Configure the Qt backend for viz tests."""
     os.environ["_MNE_BROWSER_NO_BLOCK"] = "true"
+    if "_MNE_BROWSER_BACK" not in os.environ:
+        os.environ["_MNE_BROWSER_BACK"] = "true"
 
 
 @pytest.fixture(scope="session")
diff --git a/mne/viz/backends/_qt.py b/mne/viz/backends/_qt.py
--- a/mne/viz/backends/_qt.py
+++ b/mne/viz/backends/_qt.py
@@ -720,6 +720,10 @@ def __init__(self, parent=None, title=None, size=None):
             MainWindow.__init__(self, parent=parent, title=title, size=size)
         self.setAttribute(Qt.WA_ShowWithoutActivating, True)
         self.setAttribute(Qt.WA_DeleteOnClose, True)
+        from . import renderer
+
+        if renderer.MNE_3D_BACKEND_TESTING:
+            self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnBottomHint)
 
 
 class _AppWindow(_AbstractAppWindow, _MNEMainWindow, _Widget, metaclass=_BaseWidget):
diff --git a/mne/viz/utils.py b/mne/viz/utils.py
--- a/mne/viz/utils.py
+++ b/mne/viz/utils.py
@@ -175,9 +175,12 @@ def _show_browser(show=True, block=True, fig=None, **kwargs):
     if backend == "matplotlib":
         plt_show(show, block=block, **kwargs)
     else:
+        from qtpy.QtCore import Qt
         from qtpy.QtWidgets import QApplication
         from .backends._utils import _qt_app_exec
 
+        if fig is not None and os.getenv("_MNE_BROWSER_BACK", "").lower() == "true":
+            fig.setWindowFlags(fig.windowFlags() | Qt.WindowStaysOnBottomHint)
         if show:
             fig.show()
         # If block=False, a Qt-Event-Loop has to be started
@@ -1293,6 +1296,7 @@ def _plot_sensors(
     from mpl_toolkits.mplot3d import Axes3D  # noqa: F401 analysis:ignore
     from .topomap import _get_pos_outlines, _draw_outlines
 
+    ch_names = [str(ch_name) for ch_name in ch_names]
     sphere = _check_sphere(sphere, info)
 
     edgecolors = np.repeat(rcParams["axes.edgecolor"], len(colors))


Test patch:
diff --git a/mne/tests/test_source_estimate.py b/mne/tests/test_source_estimate.py
--- a/mne/tests/test_source_estimate.py
+++ b/mne/tests/test_source_estimate.py
@@ -1911,7 +1911,7 @@ def test_scale_morph_labels(kind, scale, monkeypatch, tmp_path):
             if isinstance(scale, tuple):
                 # some other fixed constant
                 # min_, max_ = 0.84, 0.855  # zooms='auto' values
-                min_, max_ = 0.57, 0.67
+                min_, max_ = 0.55, 0.67
             elif scale == 1:
                 # min_, max_ = 0.85, 0.875  # zooms='auto' values
                 min_, max_ = 0.72, 0.76
diff --git a/requirements_testing.txt b/requirements_testing.txt
--- a/requirements_testing.txt
+++ b/requirements_testing.txt
@@ -3,6 +3,7 @@ pytest
 pytest-cov
 pytest-timeout
 pytest-harvest
+pytest-qt
 ruff
 numpydoc
 codespell