Search code examples
pythonqtqmlpytestpyside2

Testing QML based app with pytest in python


I would like to test my QML frontend code along with my Python backend code(using PySide2) with Pytest preferably, and be able to send keyClicks, MouseClicks and signals just like pytest-qt plugin does. I have already checked out pytest-qml, but the test code is written via QML, and then only ran via via pytest, but I would like to send events and such from python itself, not QML

Basically, having the python code as such:


"""
Slots, Signals, context class etc etc...
"""

app = QGuiApplication([])
engine = QQmlApplicationEngine()
engine.load(QUrl.fromLocalFile("main.qml"))
app.exec_()

and a simple main.qml file, as such,

import QtQuick 2.15
import QtQuick.Layouts 1.15

import QtQuick.Window 2.2
import QtQuick.Controls 2.15


ApplicationWindow {
    id: mywin
    width: Screen.desktopAvailableWidth
    height: Screen.desktopAvailableHeight
    visible: true
    FileDialog {
            id: openDialog
            title: "mydialog"
            onAccepted: {
            }
        }
    Button {
        objectName: "mybtn"
        width: 200
        height: 200
        id: btn
        text: "hello"
        onClicked: {
            openDialog.open()
        }
    }
}

I would like to do (pseudo-code)something like

def test_file_open():
    #Grab QQuickItem(btn)
    #Send mouse event to click btn
    #Send string to file dialog
    # assert string sent ==  string selected

The pytest-qt plugin would work, but functions take QWidget and QML deals with QQuickItems, which as far as I know doesnt deal with QWidgets.

Is it even possible, or my only option to test my app slots etc is via the pytest-qml ? Perhaps its the easiest way, but perhaps there are other options :)

Edit:

If you use import Qt.labs.platform 1.1 instead of the import QtQuick.Dialogs 1.3, and force QML to not use native dialog, then just change

    # assert myfiledialog.property("fileUrl").toLocalFile() == filename  # uses QDialog
    assert myfiledialog.property("currentFile").toLocalFile() == filename # using QLabs Dialog

And then using the rest of the code from accepted answer it will work, so apparently its very important that it does not use a native dialog.

If anyone else in the future knows how to make it work with native dialog and using QtQuick.Dialogs 1.3 as the original question presented, it would be nice :). But this is still nice to test overall!


Solution

  • You can use the same API since pytest-qt is based on QtTest. Obviously you must understand the structure of the application, for example that the FileDialog is just a QObject that only manages a QWindow that has the dialog, in addition to managing the positions of the items with respect to the windows.

    import os
    from pathlib import Path
    
    from PySide2.QtCore import QUrl
    from PySide2.QtQml import QQmlApplicationEngine
    
    CURRENT_DIR = Path(__file__).resolve().parent
    
    
    def build_engine():
        engine = QQmlApplicationEngine()
        filename = os.fspath(CURRENT_DIR / "main.qml")
        url = QUrl.fromLocalFile(filename)
        engine.load(url)
        return engine
    
    
    def main():
        app = QGuiApplication([])
        engine = build_engine()
        app.exec_()
    
    
    if __name__ == "__main__":
        main()
    
    import QtQuick 2.15
    import QtQuick.Controls 2.15
    import QtQuick.Dialogs 1.3
    import QtQuick.Layouts 1.15
    import QtQuick.Window 2.2
    
    ApplicationWindow {
        id: mywin
    
        width: Screen.desktopAvailableWidth
        height: Screen.desktopAvailableHeight
        visible: true
    
        FileDialog {
            id: openDialog
    
            objectName: "myfiledialog"
            title: "mydialog"
            onAccepted: {
            }
        }
    
        Button {
            id: btn
    
            objectName: "mybtn"
            width: 200
            height: 200
            text: "hello"
            onClicked: {
                openDialog.open();
            }
        }
    
    }
    
    import os
    
    from PySide2.QtCore import QCoreApplication, QObject, Qt, QPointF
    from PySide2.QtGui import QGuiApplication
    from PySide2.QtQuick import QQuickItem
    from PySide2.QtWidgets import QApplication
    
    import pytest
    
    from app import build_engine
    
    
    @pytest.fixture(scope="session")
    def qapp():
        QCoreApplication.setOrganizationName("qapp")
        QCoreApplication.setOrganizationDomain("qapp.com")
        QCoreApplication.setAttribute(Qt.AA_DontUseNativeDialogs)
        yield QApplication([])
    
    
    def test_app(tmp_path, qtbot):
        engine = build_engine()
    
        assert QCoreApplication.testAttribute(Qt.AA_DontUseNativeDialogs)
        
        with qtbot.wait_signal(engine.objectCreated, raising=False):
            assert len(engine.rootObjects()) == 1
        root_object = engine.rootObjects()[0]
        root_item = root_object.contentItem()
    
        mybtn = root_object.findChild(QQuickItem, "mybtn")
        assert mybtn is not None
    
        center = QPointF(mybtn.width(), mybtn.height()) / 2
        qtbot.mouseClick(
            mybtn.window(),
            Qt.LeftButton,
            pos=root_item.mapFromItem(mybtn, center).toPoint(),
        )
        qtbot.wait(1000)
        qfiledialog = None
        for window in QGuiApplication.topLevelWindows():
            if window is not root_object:
                qfiledialog = window
        assert qfiledialog is not None, QGuiApplication.topLevelWindows()
    
        file = tmp_path / "foo.txt"
        file.touch()
        filename = os.fspath(file)
    
        for letter in filename:
            qtbot.keyClick(qfiledialog, letter, delay=100)
    
        qtbot.wait(1000)
    
        qtbot.keyClick(qfiledialog, Qt.Key_Return)
    
        qtbot.wait(1000)
    
        myfiledialog = root_object.findChild(QObject, "myfiledialog")
        assert myfiledialog is not None
    
        assert myfiledialog.property("fileUrl").toLocalFile() == filename
    

    Note: The test may fail if the filedialog uses the native window, you could use tools like pyinput but a simpler option is to use virtualenv.