Search code examples
pythonpyqtpyqt5pyautoguipytest-qt

How to automate mouse drag using pytest-qt?


I have a pyqt window which tracks mouse movement while the mouse is pressed. I'm trying to write a test to automate this movement using pytest-qt.

Here is an example class:

from PyQt5.QtWidgets import *
from PyQt5.QtGui import QCursor
from PyQt5.QtWidgets import QApplication

class Tracker(QDialog):
    def __init__(self, parent=None):
        super(Tracker, self).__init__(parent)
        self.location = None
        self.cur = QCursor()
        layout = QVBoxLayout()
        self.label = QLabel()
        layout.addWidget(self.label)
        self.setLayout(layout)
        self.setModal(True)
        self.showFullScreen()

    def mouseReleaseEvent(self, e):
        x = self.cur.pos().x()
        y = self.cur.pos().y()
        self.location = (x, y)
        return super().mouseReleaseEvent(e)

    def mouseMoveEvent(self, e):
        x = self.cur.pos().x()
        y = self.cur.pos().y()
        self.label.setText(f'x: {x}, y: {y}')
        return super().mouseMoveEvent(e)

if __name__ == '__main__':

    import sys

    app = QApplication(sys.argv)
    window = Tracker()
    sys.exit(app.exec_())

I'd like to write a test case that opens the window then drags the mouse 100 pixels to the right and releases.

Here's what I've tried:

track = Tracker()
qtbot.mousePress(track, QtCore.Qt.LeftButton, pos=QPoint(300, 300))
qtbot.mouseMove(track, pos=QPoint(400, 300))
qtbot.mouseRelease(track, QtCore.Qt.LeftButton)
assert track.location == (400, 300)

I've also tried using pyautogui:

track = Tracker()
x, y = pyautogui.position()
pyautogui.dragTo(x + 100, y, button='left')
assert track.location == (x + 100, y)

When running the test it appears the left button of the mouse is not held down while dragging. The label will not update and location attribute doesn't change.


Solution

  • - Using qtbot.mouseMove():

    pytest-qt makes a wrapper of QtTest, that is, the function qtbot.mouseMove() is the same as QTest::mouseMove(). And this function has a bug that is reported QTBUG-5232 that will be fixed in Qt6/PyQt6, In the comments of the report there are several a workaround that is to replace that function by emulating QMouseEvent that will not make the cursor move but if it calls the mouseMoveEvent method so that in order to work correctly you will have to modify your code.

    from PyQt5.QtWidgets import QApplication, QDialog, QLabel, QVBoxLayout
    
    
    class Tracker(QDialog):
        def __init__(self, parent=None):
            super(Tracker, self).__init__(parent)
            self.location = None
            self.label = QLabel()
    
            layout = QVBoxLayout(self)
            layout.addWidget(self.label)
            self.setModal(True)
            self.showFullScreen()
    
        def mouseReleaseEvent(self, e):
            pos = self.mapToGlobal(e.pos())
            self.location = pos.x(), pos.y()
            return super().mouseReleaseEvent(e)
    
        def mouseMoveEvent(self, e):
            pos = self.mapToGlobal(e.pos())
            self.label.setText(f"x: {pos.x()}, y: {pos.y()}")
            return super().mouseMoveEvent(e)
    
    
    if __name__ == "__main__":
    
        import sys
    
        app = QApplication(sys.argv)
        window = Tracker()
        sys.exit(app.exec_())
    
    def test_emulate_QMouseEvent(qtbot):
        start_pos, end_pos = QtCore.QPoint(300, 300), QtCore.QPoint(400, 300)
    
        track = Tracker()
    
        def on_value_changed(value):
            event = QtGui.QMouseEvent(
                QtCore.QEvent.MouseMove,
                value,
                QtCore.Qt.NoButton,
                QtCore.Qt.LeftButton,
                QtCore.Qt.NoModifier,
            )
            QtCore.QCoreApplication.sendEvent(track, event)
    
        animation = QtCore.QVariantAnimation(
            startValue=start_pos, endValue=end_pos, duration=5000
        )
        qtbot.mousePress(track, QtCore.Qt.LeftButton, pos=QtCore.QPoint(300, 300))
        animation.valueChanged.connect(on_value_changed)
        with qtbot.waitSignal(animation.finished, timeout=10000):
            animation.start()
        qtbot.mouseRelease(track, QtCore.Qt.LeftButton)
        track.location == (end_pos.x(), end_pos.y())
    

    - Using pyautogui:

    With this method it is not necessary to make any changes.

    def test_pyautogui(qtbot):
        start_pos, end_pos = QtCore.QPoint(300, 300), QtCore.QPoint(400, 300)
        track = Tracker()
        qtbot.waitUntil(track.isVisible)
    
        def on_value_changed(value):
            pyautogui.dragTo(value.x(), value.y(), button="left")
    
        animation = QtCore.QVariantAnimation(
            startValue=start_pos, endValue=end_pos, duration=5000
        )
        pyautogui.moveTo(start_pos.x(), start_pos.y())
        pyautogui.mouseDown(button="left")
        animation.valueChanged.connect(on_value_changed)
        with qtbot.waitSignal(animation.finished, timeout=10000):
            animation.start()
    
        track.location == (end_pos.x(), end_pos.y())