2

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.

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Manuninho
  • 31
  • 1
  • 6
  • We added a few UI tests for our application and had a similar problem that we could not reliably trigger buttons in message boxes so we added a layer into our code that allows us to skip the message boxes in a test scenario. Maybe a similar approach could solve your problem. – Zaiborg Jul 15 '21 at 12:38
  • Sadly I cannot skip those message boxes, because sometimes I need to press yes or no, deppending on the test. Thanks anyway :D – Manuninho Jul 16 '21 at 06:05
  • That is the same problem as we had. And instead of showing the message box our abstraction layer would then be set so that each test would 'simulate' the desired clicks on the message boxes. – Zaiborg Jul 16 '21 at 07:30
  • I would like to understand more about your approach, because if I am facing the same problem as you did, your solution would be nice to have as well, if you can share something more to me. – Manuninho Jul 16 '21 at 08:24
  • 1
    Ill gather the details and will post it soon. – Zaiborg Jul 16 '21 at 10:12

2 Answers2

1

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 :)

Zaiborg
  • 2,492
  • 19
  • 28
  • Well, in my case, I don't think I need the nTimesExecCalled, I always have to press yes (but feel free to correct me if I'm wrong). But your idea was really good and you solved my problem, thanks a lot :D – Manuninho Jul 19 '21 at 09:06
  • 1
    Glad i could help. The body of the function is up to you so i made something up to show its potential. :) – Zaiborg Jul 19 '21 at 09:09
0

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.

jdfa
  • 659
  • 4
  • 11