pytest-dev / pytest-qt

pytest plugin for Qt (PyQt5/PyQt6 and PySide2/PySide6) application testing
https://pytest-qt.readthedocs.io
MIT License
409 stars 70 forks source link

BUG: QTest Press/Click not Working #428

Open adam-grant-hendry opened 2 years ago

adam-grant-hendry commented 2 years ago

Referencing this SO issue, I am experiencing the same problem, even with setMouseTracking enabled (in windows mode, not headless). It appears as if mousePress and mouseClick don't properly release mouse buttons.

MRE

To get hovering to function, I am forced to use mousePress after moves when I shouldn't have to.

def test_menubar_toolbar_hover_triggers_statusbar_messages(app: MainApp, qtbot: QtBot) -> None:
    """Test for correct status bar messages when items are hovered.

    For example, when the user clicks 'File' in the menubar and hovers over 'New', the
    statusbar message should read 'Create a new project...'. This test currently does not
    pass

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

    menubar = window.menubar
    toolbar = window.toolbar
    statusbar = window.statusbar
    file_menu = window.file_menu
    new_action = window.new_action
    new_button = toolbar.widgetForAction(new_action)

    qtbot.addWidget(menubar)
    qtbot.addWidget(file_menu)
    qtbot.addWidget(toolbar)
    qtbot.addWidget(new_button)

    menubar.setMouseTracking(True)
    file_menu.setMouseTracking(True)
    toolbar.setMouseTracking(True)
    new_button.setMouseTracking(True)

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

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

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

    # Act - Menubar
    qtbot.wait(10)  # In non-headless mode, give time for previous test to finish
    qtbot.mouseMove(menubar, file_rect.center())
    qtbot.mouseClick(menubar, QtCore.Qt.LeftButton, pos=file_rect.center())
    qtbot.mouseMove(file_menu, new_rect.center())

    # Assert - Menubar
    qtbot.waitUntil(check_status)

    # Act - Toolbar
    qtbot.wait(10)  # In non-headless mode, give time for previous test to finish
    qtbot.mouseMove(new_button)

    # Assert - Toolbar
    qtbot.waitUntil(check_status)

Solution

pyqtgraph has implemented a solution that works: write custom mouse movement methods instead of wrapping QTest:

# Alternative: replace qt_api with qtpy
# from qtpy import QtCore, QtGui, QtTest, QtWidgets

def mousePress(widget, pos, button, modifier=None):
    if isinstance(widget, qt_api.QtWidgets.QGraphicsView):
        widget = widget.viewport()
    if modifier is None:
        modifier = qt_api.QtCore.Qt.KeyboardModifier.NoModifier
    event = qt_api.QtGui.QMouseEvent(qt_api.QtCore.QEvent.Type.MouseButtonPress, pos, button, qt_api.QtCore.Qt.MouseButton.NoButton, modifier)
    qt_api.QtWidgets.QApplication.sendEvent(widget, event)

def mouseRelease(widget, pos, button, modifier=None):
    if isinstance(widget, qt_api.QtWidgets.QGraphicsView):
        widget = widget.viewport()
    if modifier is None:
        modifier = qt_api.QtCore.Qt.KeyboardModifier.NoModifier
    event = qt_api.QtGui.QMouseEvent(qt_api.QtCore.QEvent.Type.MouseButtonRelease, pos, button, qt_api.QtCore.Qt.MouseButton.NoButton, modifier)
    qt_api.QtWidgets.QApplication.sendEvent(widget, event)

def mouseMove(widget, pos, buttons=None, modifier=None):
    if isinstance(widget, qt_api.QtWidgets.QGraphicsView):
        widget = widget.viewport()
    if modifier is None:
        modifier = qt_api.QtCore.Qt.KeyboardModifier.NoModifier
    if buttons is None:
        buttons = qt_api.QtCore.Qt.MouseButton.NoButton
    event = qt_api.QtGui.QMouseEvent(qt_api.QtCore.QEvent.Type.MouseMove, pos, qt_api.QtCore.Qt.MouseButton.NoButton, buttons, modifier)
    qt_api.QtWidgets.QApplication.sendEvent(widget, event)

def mouseDrag(widget, pos1, pos2, button, modifier=None):
    mouseMove(widget, pos1)
    mousePress(widget, pos1, button, modifier)
    mouseMove(widget, pos2, button, modifier)
    mouseRelease(widget, pos2, button, modifier)

def mouseClick(widget, pos, button, modifier=None):
    mouseMove(widget, pos)
    mousePress(widget, pos, button, modifier)
    mouseRelease(widget, pos, button, modifier)
adam-grant-hendry commented 2 years ago

Reviewing:

  1. Qt6 QTest Namespace
  2. Qt5 QTest Namespace
  3. PySide2.QtTest Note
  4. PySide2 QTest Namespace
  5. PySide6.QtTest Note
  6. PySide6 QTest Namespace

and the available QWidget virtual functions (Python)/protected functions (C++) in

  1. Qt5
  2. Qt6
  3. PySide2
  4. PySide6

it is evident that

  1. QWidget objects do not support mouseClick (that mouseClick can be called without breaking seems an accident)
  2. mouseClick is only available for C++ (Qt5/Qt6) in QTest
  3. Many methods (namely all mouse and key events) were not bound from C++ when making PyQt5, PySide2, PyQt6, andPySide6

All of the above (plus the referenced SO post by pyqtgraph) give me strong reason to suggest:

  1. No longer use QTest.mouseClick() (minimal change)
  2. Switch from QTest mouse and key methods to QtGui mouse and key events (as it is unlikely they will continue to work properly and/or be provided in python bindings of Qt in the future).