pythonpyqtpyqt5pytestpytest-qt

How to click on QMessageBox with pytest-qt?


I am creating some unit tests for a PyQt application with pytest-qt. And I would like to create open the graphical window, do some tests then close the window, rather than open a new window for every test, ie. use a module fixture for the window itself. I succeeded to do this part, by calling in a local function a QtBot rather than using the default fixture, and removing the mocker ones. So I am pretty close to my objective.

However, but I am not able to close the window (and test the QMessageBox for closing event).

I red examples like how to handle modal dialog and its git discussion, or the qmessage question; which seem to be close to my question. It is suggested to use a timer to wait for the QMessageBox to appear then click on a button choice, but visibly I am not able to apply them correctly. In my attempt, pytest get the closing demand, but not the click on the dialog box. So, I have to click myself to finish the test.

Here is a small example, with file GUI.py:

#!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys

from PyQt5 import QtGui, QtCore, QtWidgets

from PyQt5.QtWidgets import *
from PyQt5.QtCore import QCoreApplication, Qt, QObject
from PyQt5.QtGui import QIcon


class Example(QMainWindow):
    def __init__(self, parent = None):
        super().__init__()
        self.initUI(self)

    def initUI(self, MainWindow):
        # centralwidget
        MainWindow.resize(346, 193)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        # The Action to quit
        self.toolb_action_Exit = QAction(QIcon('exit.png'), 'Exit', self)
        self.toolb_action_Exit.setShortcut('Ctrl+Q')
        self.toolb_action_Exit.triggered.connect(self.close)

        # The Button
        self.btn_prt = QtWidgets.QPushButton(self.centralwidget)
        self.btn_prt.setGeometry(QtCore.QRect(120, 20, 89, 25))
        self.btn_prt.clicked.connect(lambda: self.doPrint() )
        self.btn_quit = QtWidgets.QPushButton(self.centralwidget)
        self.btn_quit.setGeometry(QtCore.QRect(220, 20, 89, 25))
        self.btn_quit.clicked.connect(lambda: self.close() )

        # The textEdit
        self.textEdit = QtWidgets.QTextEdit(self.centralwidget)
        self.textEdit.setGeometry(QtCore.QRect(10, 60, 321, 81))

        # Show the frame
        MainWindow.setCentralWidget(self.centralwidget)
        self.show()

    def doPrint(self):
        print('TEST doPrint')

    def closeEvent(self, event):
        # Ask a question before to quit.
        self.replyClosing = QMessageBox.question(self, 'Message',
            "Are you sure to quit?", QMessageBox.Yes |
            QMessageBox.No, QMessageBox.No)

        if self.replyClosing == QMessageBox.Yes:
            event.accept()
        else:
            event.ignore()


def main_GUI():
    app = QApplication(sys.argv)
    imageViewer = Example()
    return app, imageViewer


if __name__ == '__main__':
    app, imageViewer =main_GUI()
    rc= app.exec_()
    print('App end is exit code {}'.format(rc))
    sys.exit(rc)

and the pytest file named test_GUI.py:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os, sys
import pytest

from PyQt5 import QtGui, QtCore, QtWidgets, QtTest
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QCoreApplication, Qt, QObject

from pytestqt.plugin import QtBot

GUI = __import__('GUI')


@pytest.yield_fixture(scope="module")
def qtbot_session(qapp, request):
    print("  SETUP qtbot")
    result = QtBot(qapp)
    with capture_exceptions() as exceptions:
        yield result
    print("  TEARDOWN qtbot")


@pytest.fixture(scope="module")
def Viewer(request):
    print("  SETUP GUI")
    app, imageViewer = GUI.main_GUI()
    qtbotbis = QtBot(app)
    # qtbotbis.addWidget(imageViewer)
    # qtbotbis.wait_for_window_shown(imageViewer)
    QtTest.QTest.qWait(0.5 *1000)
    yield app, imageViewer, qtbotbis

    # EXIT
    # mocker.patch.object(QMessageBox, 'question', return_value=QMessageBox.Yes)
    # imageViewer.toolb_action_Exit.trigger()
    def handle_dialog():
        # while not imageViewer.replyClosing.isVisible():
        #   app.processEvents()
        box = QMessageBox()
        box.setStandardButtons(QMessageBox.Yes)
        button = box.button(QMessageBox.Yes)
        qtbotbis.mouseClick(button, QtCore.Qt.LeftButton)
    QtCore.QTimer.singleShot(100, handle_dialog)
    qtbotbis.mouseClick(imageViewer.btn_quit, QtCore.Qt.LeftButton, delay=1)
    assert imageViewer.close()
    print("  TEARDOWN GUI")


class Test_GUI() :
    def test_interface(self, Viewer):
        print("  beginning ")
        app, imageViewer, qtbot = Viewer
        qtbot.mouseClick( imageViewer.btn_prt, QtCore.Qt.LeftButton )
        QtTest.QTest.qWait(0.5 *1000)
        assert True
        print(" Test passed")

Any idea of what I am missing ? Any other idea or suggestion would also be appreciate.


Solution

  • In your attempt you are creating a new QMessageBox that is different from the one created with the static method QMessageBox::question() so even if you click it will not work.

    The idea is to obtain the QMessageBox shown, and in this case we will take advantage of that since it is the active window so we can obtain it using QApplication::activeWindow(). Another way to get the QMessageBox is to use the relationship between imageViewer and the QMessageBox through findChild():

    @pytest.fixture(scope="module")
    def Viewer(request):
        print("  SETUP GUI")
    
        app, imageViewer = GUI.main_GUI()
        qtbotbis = QtBot(app)
        QtTest.QTest.qWait(0.5 * 1000)
    
        yield app, imageViewer, qtbotbis
    
        def handle_dialog():
            messagebox = QtWidgets.QApplication.activeWindow()
            # or
            # messagebox = imageViewer.findChild(QtWidgets.QMessageBox)
            yes_button = messagebox.button(QtWidgets.QMessageBox.Yes)
            qtbotbis.mouseClick(yes_button, QtCore.Qt.LeftButton, delay=1)
    
        QtCore.QTimer.singleShot(100, handle_dialog)
        qtbotbis.mouseClick(imageViewer.btn_quit, QtCore.Qt.LeftButton, delay=1)
        assert imageViewer.isHidden()