Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

QMessageBox::question returning NoButton?



  • I am running pytest and PyQt-5 with QT_QPA_PLATFORM=offscreen and QMessageBox::question is returning NoButton, but only when many other unit tests are running first. The test runs fine alone. I have debugged the qt code from QMessageBox::question on down and for some reason QMessageBoxPrivate::_q_buttonClicked is never called.

    I can't include an example here because this is a complicated integration test. But it is clear that the QEventLoop in QDialog::exec is not executing anything.

    Under what conditions would this occur? The documentation doesn't help. Thanks!



  • @patrickkidd Turns out my code was calling QApplication::quit at some point in one of the prior unit tests. So the answer is that QMessageBox::question (and the other static methods, and probably also QDialog) quietly returns the default - NoButton - when the application has quit.



  • @patrickkidd Turns out my code was calling QApplication::quit at some point in one of the prior unit tests. So the answer is that QMessageBox::question (and the other static methods, and probably also QDialog) quietly returns the default - NoButton - when the application has quit.


  • Lifetime Qt Champion

    Hi,

    Out of curiosity, are you monkeypatching your QMessageBox for the tests ?

    Otherwise how are you making the that part work while testing ?



  • @SGaist said in QMessageBox::question returning NoButton?:

    Hi,

    Out of curiosity, are you monkeypatching your QMessageBox for the tests ?

    Otherwise how are you making the that part work while testing ?

    qtbot.clickYesAfter(lambda: model.removeTag(1))
    
    from pytestqt.qtbot import QtBot
    
    class PKQtBot(QtBot):
    
        def qWaitForMessageBox(self, action, contains=None, handleClick=None):
            from PyQt5.QtWidgets import QAbstractButton
            msgBoxAccepted = util.Condition()
            def acceptMessageBox():
                box = QApplication.activeModalWidget()
                if box:
                    if contains:
                        assert contains in box.text()
                    if handleClick:
                        handleClick()
                        msgBoxAccepted()
                        msgBoxAccepted.timer.stop()
                    else:
                        okButton = box.button(QMessageBox.Ok)
                        box.buttonClicked[QAbstractButton].connect(msgBoxAccepted)
                        msgBoxAccepted()
                        self.mouseClick(okButton, Qt.LeftButton)
                        msgBoxAccepted.timer.stop()
            msgBoxAccepted.timer = QTimer(QApplication.instance())
            msgBoxAccepted.timer.timeout.connect(acceptMessageBox)
            msgBoxAccepted.timer.start(100)
            action()
            msgBoxAccepted.wait()
        
        def clickYesAfter(self, action):
            def doClickYes():
                self.mouseClick(QApplication.activeModalWidget().button(QMessageBox.Yes),
                                Qt.LeftButton)
            self.qWaitForMessageBox(action, handleClick=doClickYes)
            
        def clickNoAfter(self, action):
            def doClickNo():
                self.mouseClick(QApplication.activeModalWidget().button(QMessageBox.No),
                                Qt.LeftButton)
            self.qWaitForMessageBox(action, handleClick=doClickNo)
    
    
    @pytest.yield_fixture
    def qtbot(qApp, request):
        """ Overridden to use our qApp, because the old one was calling abort(). """
        result = PKQtBot(request)
        util.qtbot = result
        yield result
    

    I also have another one that uses my custom Condition class, which can be used to wait for a lambda or keep track of signal calls:

        def waitUntil(self, condition, timeout=2000):
            util.Condition(condition=condition).wait(maxMS=timeout)
    
    class Condition(Debug):
        """ Allows you to wait for a signal to be called. """
        def __init__(self, only=None, condition=None):
            self.callCount = 0
            self.callArgs = []
            self.senders = []
            self.lastCallArgs  = None
            self.only = only
            self.condition = condition
    
        def reset(self):
            self.callCount = 0
            self.callArgs = []
            self.senders = []
            self.lastCallArgs = None
    
        def test(self):
            """ Return true if the condition is true. """
            if self.condition:
                return self.condition()
            else:
                self.callCount > 0
    
        def set(self, *args):
            """ Set the condition to true. Alias for condition(). """
            self.callCount += 1
            self.senders.append(QObject().sender())
            self.lastCallArgs = args
            self.callArgs.append(args)
    
        def __call__(self, *args):
            """ Called by whatever signal that triggers the condition. """
            if self.only:
                only = self.only
                if not only(*args):
                    return
            self.set(*args)
    
        def wait(self, maxMS=2000, onError=None):
            """ Wait for the condition to be true. onError is a callback. """
            startTime = time.time()
            success = True
            app = QApplication.instance()
            while app and not self.test():
                try:
                    app.processEvents(QEventLoop.WaitForMoreEvents, 100)
                except KeyboardInterrupt as e:
                    if onError:
                        onError()
                    break
                elapsed = ((time.time() - startTime) * 1000)
                if elapsed >= maxMS:
                    break
    

Log in to reply