Home > database >  Testing QML based app with pytest in python
Testing QML based app with pytest in python

Time:04-15

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!

CodePudding user response:

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.

  • Related