Click on button for several QMessageBox in Qtest

Viewed 235

I'm creating a test for my GUI application. At a certain point of the test, I want to click on a button which asks for user's confirmation and then, confirmation for each file I have to delete. So, on the test, to press that button I'm doing:

QTest::mouseClick(m_widget->removeButton, Qt::LeftButton);

But now, for the first QMessageBox I get I'm able to click on yes with:

QKeyEvent *evr = new QKeyEvent(QEvent::KeyPress, Qt::Key_Return, Qt::NoModifier);
QApplication::postEvent(&m_widget->reply, evr);

From here, I have to ask for confirmation for each file I want to delete, but I'm not able to execute anything else until I click by me myself with the mouse or with any solution I'm trying to find. I observed with the qDebug that it won't go further of that mouseClick function until all those QMessageBox are clicked (it could be one or more).

All the QMessageBox are local variable on the app, nothing is static.

2 Answers

We resolved a similar issue by adding a layer of abstraction over the message boxes. We have a global object with functions to 'display' message boxes and dialogs as follows:

struct QtFuncs
{
    typedef std::function<int(QMessageBox*)> MessageBoxExec;
    MessageBoxExec messageBoxExec = [](QMessageBox* mb) { return mb->exec(); };
    // more functions for dialogs and standard message boxes (open file, ...)
};
struct QtGlobalFuncs
{
    static QtFuncs& instance()
    {
        static auto fn = QtFuncs();
        return fn;
    }

    static int messageBoxExec(QMessageBox* box)
    {
        return instance().messageBoxExec(box);
    }
};

And when we want/need to execute a message box we would 'exec' it via:

QMessageBox box(QMessageBox::Critical, "hi", "do you want to greet bob?", QMessageBox::Yes | QMessageBox::No);
auto button = QtGlobalFuncs::messageBoxExec(&box);

Note that this method requires you to replace all QMessageBox::exec calls with QtGlobalFuncs::messageBoxExec. In our test scenario we would then overwrite the internal function:

int nTimesExecCalled = 0;
QtGlobalFuncs::instance().messageBoxExec = [&nTimesExecCalled](auto box)
{
    int res = QMessageBox::Yes;
    if (nTimesExecCalled)
        res = QMessageBox::No;

    ++nTimesExecCalled;
    return res;
};

QMessageBox box(QMessageBox::Critical, "hi", "do you want to greet bob?", QMessageBox::Yes | QMessageBox::No);
auto button = QtGlobalFuncs::messageBoxExec(&box);

I hope this small example helps you understand how we resolved this problem for us and maybe it will help you as well.

Have a nice day :)

I have one solution for this problem which not requires any modification on production code. Main issue in this flow is that QMessageBox is usually being called with it's own message loop (by exec() method). That means direct solution like this will not work:

p_button->show();
QTest::qWaitForWindowActive(p_btn);
QTest::mouseClick(p_button, Qt::LeftButton);//connected to msgbox.exec() 
//next row will be not executed since we are still in event loop of msgbox 
QTest::keyEvent(QTest::Click, qApp->activeWindow(), Qt::Key_Return);

So you need to expect that QMessageBox before it appears. One way to do that is to create eventFilter which will look for activated QMessageBox. In that way you can also validate QMessageBox properties if that required.

Imagine you have such function:

QAbstractButton* CreateWidget(QWidget* ip_parent)
    {
      auto p_btn = new QPushButton(ip_parent);
      QObject::connect(p_btn, &QAbstractButton::pressed, []() {
        QMessageBox msgBox;
        msgBox.setText("Are you sure?");
        msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
        msgBox.exec();
      });
    
      return p_btn;
    }

It will create button which will execute QMessageBox on press. To test it you can use helper like this one:

class MessageWatcher : public QObject {
public:
    using tDialogChecker = std::function<void(QMessageBox*)>;

    MessageWatcher(tDialogChecker i_checker, QObject* ip_parent = nullptr)
        : QObject(ip_parent)
        , m_checker(i_checker)
    {
        qApp->installEventFilter(this);
    }

    bool eventFilter(QObject* ip_obj, QEvent* ip_event) override
    {
        if (auto p_dlg = qobject_cast<QMessageBox*>(ip_obj)) {
            if (ip_event->type() == QEvent::WindowActivate) { 
                m_checker(p_dlg);
                return true;
            }
        }
        return false;
    }

private:
    tDialogChecker m_checker;
};

It will call lambda when it will receive event of type QEvent::WindowActivate for QMessageBox. You can perform any checks related to QMessageBox itself and you can execute closure of that QMessageBox there. Consider this simple test:

class WidgetLibTest : public QObject {
    Q_OBJECT
private slots:
    void WidgetLibCheck();
};

void WidgetLibTest::WidgetLibCheck()
{
    MessageWatcher watcher([](auto ip_msg_box)
        {
        auto closer = qScopeGuard([ip_msg_box] { QTest::keyEvent(QTest::Click, ip_msg_box, Qt::Key_Return); });
        QCOMPARE(ip_msg_box->text(), "Are you sure?");
        });
    
    auto p_btn = std::unique_ptr<QAbstractButton>(CreateWidget(nullptr));
    p_btn->show();

    QTest::qWaitForWindowActive(p_btn.get());
    QTest::mouseClick(p_btn.get(), Qt::LeftButton);//will execute QMessageBox
}

qScopeGuard is needed because when QCOMPARE is failed it will call return which will skip rest of the code. So, to close QMessageBox every time, even when check is not correct, you need to use it.

In similar way you can also test QProgressDialog or any dialog which is going to pop-up in your entangled implementation. Also it is possible to test cascade of widgets\dialogs, only in that case you need some kind of array of functors. But I would suggest to avoid that situation and restructure implementation so it will be possible to test each component separately.

Related