PyQt5 не может протестировать всплывающую подсказку в автономном режиме в Windows

Я использую GitLab для CI, и мне нужно протестировать графический интерфейс PyQt5 в автономном режиме с помощью pytest-qt(Я использую Python 3.8 в Windows 10). С этой целью я могу работать в безголовом режиме, установив переменную среды QT_QPA_PLATFORMк "offscreen"в моем pyproject.toml:

      [tool.pytest.ini_options]
env = [
    "D:QT_QPA_PLATFORM=offscreen"
]

и следующий тест проходит успешно при запуске в оконном режиме, но тест всплывающей подсказки не проходит в автономном режиме. Как я могу сделать этот проход в безголовом режиме?:

tests/test_view.py

      def test_toolbar_statusbar_and_tooltip_messages(app: MainApp, qtbot: QtBot) -> None:
    """Test for correct status bar message when a toolbar item is hovered.

    For example, when the user clicks 'File' in the menubar and hovers over 'New', the
    statusbar tooltip should read 'Create a new project...'.

    Args:
        app (MainApp): (fixture) Qt main application
        qtbot (QtBot): (fixture) Bot that imitates user interaction
    """
    # Arrange
    window = app.view

    statusbar = window.statusbar
    toolbar = window.toolbar
    new_action = window.new_action

    new_rect = toolbar.actionGeometry(new_action)
    tooltip = QtWidgets.QToolTip

    # Assert - Precondition
    assert statusbar.currentMessage() == ''
    assert tooltip.text() == ''

    # Act
    qtbot.mouseMove(toolbar, new_rect.center())

    # Assert - Postcondition
    def check_status():
        assert statusbar.currentMessage() == 'Create a new project...'

    def check_tooltip():
        assert tooltip.text() == 'New Project'

    qtbot.waitUntil(check_status)
    qtbot.waitUntil(check_tooltip)

Вот оставшийся код для MRE:

tests/conftest.py

      from typing import Generator, Union, Sequence

import pytest
from pytestqt.qtbot import QtBot
from qtpy import QtCore

from myproj.main import MainApp

# Register plugins to use in testing
pyteset_plugins: Union[str, Sequence[str]] = [
    'pytestqt.qtbot',
]

@pytest.fixture(autouse=True)
def clear_settings() -> Generator[None, None, None]:
    """Fixture to clear ``Qt`` settings."""
    yield
    QtCore.QSettings().clear()


@pytest.fixture(name='app')
def fixture_app(qtbot: QtBot) -> Generator[MainApp, None, None]:
    """``pytest`` fixture for ``Qt``.

    Args:
        qtbot (QtBot): pytest fixture for Qt

    Yields:
        Generator[QtBot, None, None]: Generator that yields QtBot fixtures
    """
    # Setup
    root = MainApp()
    root.show()
    qtbot.addWidget(root.view)

    # Run
    yield root

    # Teardown
    # None

myproj/main.py

      from pyvistaqt import MainWindow
from qtpy import QtCore, QtGui, QtWidgets
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QApplication

import resources


class View(MainWindow):
    def __init__(
        self,
        controller: 'MainApp',
    ) -> None:
        """Display GUI main window.

        Args:
            controller (): The application controller, in the model-view-controller (MVC)
                framework sense
        """
        super().__init__()
        self.controller = controller

        self.setWindowTitle('My Project')

        self.container = QtWidgets.QFrame()

        self.layout_ = QtWidgets.QVBoxLayout()
        self.layout_.setSpacing(0)
        self.layout_.setContentsMargins(0, 0, 0, 0)

        self.container.setLayout(self.layout_)
        self.setCentralWidget(self.container)

        self._create_actions()
        self._create_menubar()
        self._create_toolbar()
        self._create_statusbar()

    def _create_actions(self) -> None:
        """Create QAction items for menu- and toolbar."""
        self.new_action = QtWidgets.QAction(
            QtGui.QIcon(resources.NEW_ICO),
            '&New Project...',
            self,
        )
        self.new_action.setShortcut('Ctrl+N')
        self.new_action.setStatusTip('Create a new project...')

    def _create_menubar(self) -> None:
        """Create the main menubar."""
        self.menubar = self.menuBar()
        self.file_menu = self.menubar.addMenu('&File')
        self.file_menu.addAction(self.new_action)

    def _create_toolbar(self) -> None:
        """Create the main toolbar."""
        self.toolbar = QtWidgets.QToolBar('Main Toolbar')
        self.toolbar.setIconSize(QtCore.QSize(24, 24))
        self.addToolBar(self.toolbar)
        self.toolbar.addAction(self.new_action)

    def _create_statusbar(self) -> None:
        """Create the main status bar."""
        self.statusbar = QtWidgets.QStatusBar(self)
        self.setStatusBar(self.statusbar)


class MainApp:
    def __init__(self) -> None:
        """GUI controller."""
        self.view = View(controller=self)

    def show(self) -> None:
        """Display the main window."""
        self.view.showMaximized()


if __name__ == '__main__':
    app = QApplication([])
    app.setStyle('fusion')
    app.setAttribute(Qt.AA_DontShowIconsInMenus, True)

    root = MainApp()
    root.show()

    app.exec_()

Полученные результаты

Вот сообщение об ошибке, которое я получаю в безголовом режиме:

      PS> pytest
================================================================================================ test session starts =================================================================================================
platform win32 -- Python 3.8.10, pytest-7.1.2, pluggy-1.0.0
PyQt5 5.15.6 -- Qt runtime 5.15.2 -- Qt compiled 5.15.2
Using --randomly-seed=1234
rootdir: %USERPROFILE%\Code\myproj, configfile: pyproject.toml, testpaths: tests
plugins: hypothesis-6.46.9, cov-3.0.0, doctestplus-0.12.0, env-0.6.2, memprof-0.2.0, qt-4.0.2, randomly-3.12.0, typeguard-2.13.3
collected 5 items

tests\docs_tests\test_index_page.py .                                                                                                                                                                           [ 20%]
tests\test_view.py ...F                                                                                                                                                                                         [100%]

====================================================================================================== FAILURES ======================================================================================================
____________________________________________________________________________________ test_toolbar_statusbar_and_tooltip_messages _____________________________________________________________________________________

self = <pytestqt.qtbot.QtBot object at 0x000001C4858B78E0>, callback = <function test_toolbar_statusbar_and_tooltip_messages.<locals>.check_tooltip at 0x000001C4858BB9D0>

    def waitUntil(self, callback, *, timeout=5000):
        """
        .. versionadded:: 2.0

        Wait in a busy loop, calling the given callback periodically until timeout is reached.    

        ``callback()`` should raise ``AssertionError`` to indicate that the desired condition     
        has not yet been reached, or just return ``None`` when it does. Useful to ``assert`` until
        some condition is satisfied:

        .. code-block:: python

            def view_updated():
                assert view_model.count() > 10


            qtbot.waitUntil(view_updated)

        Another possibility is for ``callback()`` to return ``True`` when the desired condition   
        is met, ``False`` otherwise. Useful specially with ``lambda`` for terser code, but keep   
        in mind that the error message in those cases is usually not very useful because it is    
        not using an ``assert`` expression.

        .. code-block:: python

            qtbot.waitUntil(lambda: view_model.count() > 10)

        Note that this usage only accepts returning actual ``True`` and ``False`` values,
        so returning an empty list to express "falseness" raises a ``ValueError``.

        :param callback: callable that will be called periodically.
        :param timeout: timeout value in ms.
        :raises ValueError: if the return value from the callback is anything other than ``None``,
            ``True`` or ``False``.

        .. note:: This method is also available as ``wait_until`` (pep-8 alias)
        """
        __tracebackhide__ = True
        import time

        start = time.time()

        def timed_out():
            elapsed = time.time() - start
            elapsed_ms = elapsed * 1000
            return elapsed_ms > timeout

        timeout_msg = f"waitUntil timed out in {timeout} milliseconds"

        while True:
            try:
>               result = callback()

.venv\lib\site-packages\pytestqt\qtbot.py:510:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

    def check_tooltip():
>       assert tooltip.text() == 'New Project'
E       AssertionError: assert '' == 'New Project'
E         - New Project

tests\test_view.py:104: AssertionError

The above exception was the direct cause of the following exception:

app = MainApp(), qtbot = <pytestqt.qtbot.QtBot object at 0x000001C4858B78E0>

    def test_toolbar_statusbar_and_tooltip_messages(app: MainApp, qtbot: QtBot) -> None:
        """Test for correct status bar message when a toolbar item is hovered.

        For example, when the user clicks 'File' in the menubar and hovers over 'New', the
        statusbar tooltip should read 'Create a new project...'.

        Args:
            app (MainApp): (fixture) Qt main application
            qtbot (QtBot): (fixture) Bot that imitates user interaction
        """
        # Arrange
        window = app.view

        statusbar = window.statusbar
        toolbar = window.toolbar
        new_action = window.new_action

        new_rect = toolbar.actionGeometry(new_action)
        tooltip = QtWidgets.QToolTip

        # Assert - Precondition
        assert statusbar.currentMessage() == ''
        assert tooltip.text() == ''

        # Act
        qtbot.mouseMove(toolbar, new_rect.center())

        # Assert - Postcondition
        def check_status():
            assert statusbar.currentMessage() == 'Create a new project...'

        def check_tooltip():
            assert tooltip.text() == 'New Project'

        qtbot.waitUntil(check_status)
>       qtbot.waitUntil(check_tooltip)
E       pytestqt.exceptions.TimeoutError: waitUntil timed out in 5000 milliseconds

tests\test_view.py:107: TimeoutError
------------------------------------------------------------------------------------------------ Captured Qt messages ------------------------------------------------------------------------------------------------ 
QtWarningMsg: This plugin does not support propagateSizeHints()
============================================================================================ memory consumption estimates ============================================================================================ 
tests/test_view.py::test_toolbar_statusbar_and_tooltip_messages  - 13.0 MB
tests/test_view.py::test_menubar_statusbar_messages              - 136.0 KB
docs_tests::test_index_page.py::test_index[index.html]           - 128.0 KB
tests/test_view.py::test_window_appears                          - 68.0 KB
tests/test_view.py::test_title_correct                           - 68.0 KB

---------- coverage: platform win32, python 3.8.10-final-0 -----------
Name                          Stmts   Miss Branch BrPart  Cover   Missing
-------------------------------------------------------------------------
docs\source\conf.py              33      0      2      0   100%
myproj\__init__.py                  2      0      0      0   100%
myproj\main.py                     11      1      0      0    91%   15
myproj\model.py                     3      0      2      0   100%
myproj\view.py                     42      1      4      1    96%   13
resources\__init__.py             1      0      0      0   100%
resources\icons\__init__.py       4      0      0      0   100%
-------------------------------------------------------------------------
TOTAL                            96      2      8      1    97%
Coverage HTML written to dir logs/coverage/html
Coverage XML written to file logs/coverage/coverage.xml

============================================================================================== short test summary info =============================================================================================== 
FAILED tests/test_view.py::test_toolbar_statusbar_and_tooltip_messages - pytestqt.exceptions.TimeoutError: waitUntil timed out in 5000 milliseconds
============================================================================================ 1 failed, 4 passed in 6.41s =============================================================================================

Обновлять

Я также провел аналогичный тест для своих пунктов меню и обнаружил, что если я не запустил его, тест проходит в режиме без головы. Я думаю, что-то не так с моими приборами...

Тест также проходит, если я запускаю его индивидуально, например:

      PS> pytest ./tests/test_view.py::test_toolbar_statusbar_and_tooltip_messages

Вот еще один тест, который я прокомментировал:

      def test_menubar_statusbar_messages(app: MainApp, qtbot: QtBot) -> None:
    """Test for correct status bar message when a menu item is hovered.

    For example, when the user clicks 'File' in the menubar and hovers over 'New', the
    statusbar tooltip should read 'Create a new project...'.

    Args:
        app (MainApp): (fixture) Qt main application
        qtbot (QtBot): (fixture) Bot that imitates user interaction
    """
    # Arrange
    window = app.view

    menubar = window.menubar
    statusbar = window.statusbar
    file_menu = window.file_menu
    new_action = window.new_action

    file_rect = menubar.actionGeometry(file_menu.menuAction())
    new_rect = file_menu.actionGeometry(new_action)

    # Act
    qtbot.mouseMove(menubar, file_rect.center())
    qtbot.mouseClick(menubar, qt_api.QtCore.Qt.LeftButton, pos=file_rect.center())
    qtbot.mouseMove(file_menu, new_rect.center())
    qtbot.mousePress(file_menu, qt_api.QtCore.Qt.LeftButton, pos=new_rect.center())

    # Assert
    def check_status():
        assert statusbar.currentMessage() == 'Create a new project...'

    qtbot.waitUntil(check_status)

0 ответов

Другие вопросы по тегам