// Copyright (C) 2021 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include #ifndef QTEST_THROW_ON_FAIL # error This test requires QTEST_THROW_ON_FAIL being active. #endif #include #include #include #include #include #include #include #include #include #include #include using namespace QQuickVisualTestUtils; using namespace QQuickDialogTestUtils; using namespace QQuickControlsTestUtils; class tst_QQuickMessageDialogImpl : public QQmlDataTest { Q_OBJECT public: tst_QQuickMessageDialogImpl(); static void initMain() { // We need to set this attribute. QCoreApplication::setAttribute(Qt::AA_DontUseNativeDialogs); // We don't want to run this test for every style, as each one will have // different ways of implementing the dialogs. // For now we only test one style. // TODO: use Basic QQuickStyle::setStyle("Fusion"); } private slots: void changeText_data(); void changeText(); void changeInformativeText_data(); void changeInformativeText(); void changeStandardButtons(); void detailedText(); void resultReflectsLastStandardButtonPressed(); void checkModality_data(); void checkModality(); }; // We don't want to fail on warnings until QTBUG-98964 is fixed, // as we deliberately prevent deferred execution in some of the tests here, // which causes warnings. tst_QQuickMessageDialogImpl::tst_QQuickMessageDialogImpl() : QQmlDataTest(QT_QMLTEST_DATADIR, FailOnWarningsPolicy::DoNotFailOnWarnings) { } void tst_QQuickMessageDialogImpl::changeText_data() { QTest::addColumn("testString1"); QTest::addColumn("testString2"); QTest::newRow("data") << "the quick brown fox jumped over the lazy dog" << "the lazy dog jumped over the quick brown fox"; } void tst_QQuickMessageDialogImpl::changeText() { QTest::failOnWarning(QRegularExpression(".*")); QFETCH(QString, testString1); QFETCH(QString, testString2); DialogTestHelper dialogHelper( this, "messageDialog.qml"); QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); QVERIFY(dialogHelper.waitForWindowActive()); QVERIFY(dialogHelper.openDialog()); QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); QSignalSpy textSpy(dialogHelper.dialog, SIGNAL(textChanged())); QVERIFY(textSpy.isValid()); auto textLabel = dialogHelper.quickDialog->findChild("textLabel"); QVERIFY(textLabel); // default value is empty string QCOMPARE(dialogHelper.dialog->text(), ""); // update the text property dialogHelper.dialog->setText(testString1); QCOMPARE(textSpy.size(), 1); // The textLabel is empty until dialog is re-opened QCOMPARE(dialogHelper.dialog->text(), testString1); QCOMPARE(textLabel->text(), ""); dialogHelper.dialog->close(); dialogHelper.dialog->open(); QCOMPARE(dialogHelper.dialog->text(), testString1); QCOMPARE(textLabel->text(), testString1); // The textLabel isn't updated immediately dialogHelper.dialog->setText(testString2); QCOMPARE(textSpy.size(), 2); QCOMPARE(textLabel->text(), testString1); dialogHelper.dialog->close(); } void tst_QQuickMessageDialogImpl::changeInformativeText_data() { QTest::addColumn("testString1"); QTest::addColumn("testString2"); QTest::newRow("data") << "the quick brown fox jumped over the lazy dog" << "the lazy dog jumped over the quick brown fox"; } void tst_QQuickMessageDialogImpl::changeInformativeText() { QFETCH(QString, testString1); QFETCH(QString, testString2); DialogTestHelper dialogHelper( this, "messageDialog.qml"); QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); QVERIFY(dialogHelper.waitForWindowActive()); QVERIFY(dialogHelper.openDialog()); QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); QSignalSpy informativeTextSpy(dialogHelper.dialog, SIGNAL(informativeTextChanged())); QVERIFY(informativeTextSpy.isValid()); auto informativeTextLabel = dialogHelper.quickDialog->findChild("informativeTextLabel"); QVERIFY(informativeTextLabel); // default value is empty string QCOMPARE(dialogHelper.dialog->informativeText(), ""); // update the informativeText property dialogHelper.dialog->setInformativeText(testString1); QCOMPARE(informativeTextSpy.size(), 1); // The textLabel is empty until dialog is re-opened QCOMPARE(dialogHelper.dialog->informativeText(), testString1); QCOMPARE(informativeTextLabel->text(), ""); dialogHelper.dialog->close(); dialogHelper.dialog->open(); QCOMPARE(dialogHelper.dialog->informativeText(), testString1); QCOMPARE(informativeTextLabel->text(), testString1); // The textLabel shouldn't update immediately dialogHelper.dialog->setInformativeText(testString2); QCOMPARE(informativeTextSpy.size(), 2); QCOMPARE(informativeTextLabel->text(), testString1); dialogHelper.dialog->close(); } void tst_QQuickMessageDialogImpl::changeStandardButtons() { DialogTestHelper dialogHelper( this, "messageDialog.qml"); QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); QVERIFY(dialogHelper.waitForWindowActive()); QVERIFY(dialogHelper.openDialog()); QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); QSignalSpy buttonBoxSpy(dialogHelper.dialog, SIGNAL(buttonsChanged())); QVERIFY(buttonBoxSpy.isValid()); auto buttonBox = dialogHelper.quickDialog->findChild("buttonBox"); QVERIFY(buttonBox); std::bitset availableButtonRoles; auto verifyButtonBox = [&]() { availableButtonRoles.reset(); for (int i = 0; i < buttonBox->count(); ++i) { const auto button = qobject_cast(buttonBox->itemAt(i)); const auto role = QQuickDialogPrivate::buttonRole(button); QVERIFY(role > QPlatformDialogHelper::InvalidRole && role < QPlatformDialogHelper::NRoles); availableButtonRoles.set(role); } }; QCOMPARE(buttonBox->count(), 1); QVERIFY2(dialogHelper.dialog->buttons() & QPlatformDialogHelper::StandardButton::Ok, "The QMessageDialogOptionsPrivate constructor assures that the Ok button will be " "added by default, if no other buttons are set"); dialogHelper.dialog->setButtons( QPlatformDialogHelper::StandardButtons(QPlatformDialogHelper::StandardButton::Save | QPlatformDialogHelper::StandardButton::Cancel | QPlatformDialogHelper::StandardButton::Apply)); QCOMPARE(buttonBoxSpy.size(), 1); QCOMPARE(buttonBox->count(), 1); dialogHelper.dialog->close(); dialogHelper.dialog->open(); QCOMPARE(buttonBox->count(), 3); verifyButtonBox(); QVERIFY(availableButtonRoles.test(QPlatformDialogHelper::AcceptRole)); QVERIFY(availableButtonRoles.test(QPlatformDialogHelper::RejectRole)); QVERIFY(availableButtonRoles.test(QPlatformDialogHelper::ApplyRole)); // change buttons when the dialog is closed dialogHelper.dialog->close(); dialogHelper.dialog->setButtons( QPlatformDialogHelper::StandardButton(QPlatformDialogHelper::StandardButton::Ok | QPlatformDialogHelper::StandardButton::Close)); QCOMPARE(buttonBoxSpy.size(), 2); QCOMPARE(buttonBox->count(), 3); dialogHelper.dialog->open(); QCOMPARE(buttonBox->count(), 2); verifyButtonBox(); QVERIFY(availableButtonRoles.test(QPlatformDialogHelper::AcceptRole)); QVERIFY(availableButtonRoles.test(QPlatformDialogHelper::RejectRole)); dialogHelper.dialog->close(); } void tst_QQuickMessageDialogImpl::detailedText() { const QString emptyString; const QString nonEmptyString("the quick brown fox jumped over the lazy dog"); DialogTestHelper dialogHelper( this, "messageDialog.qml"); QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); QVERIFY(dialogHelper.waitForWindowActive()); QVERIFY(dialogHelper.openDialog()); QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); QSignalSpy detailedTextSpy(dialogHelper.dialog, SIGNAL(detailedTextChanged())); QVERIFY(detailedTextSpy.isValid()); auto detailedTextArea = dialogHelper.quickDialog->findChild("detailedText"); QVERIFY(detailedTextArea); auto detailedTextButton = dialogHelper.quickDialog->findChild("detailedTextButton"); QVERIFY(detailedTextButton); // Should be empty by default QCOMPARE(dialogHelper.dialog->detailedText(), emptyString); QCOMPARE(detailedTextArea->text(), emptyString); QVERIFY(!detailedTextButton->isVisible()); // Set the detailed text to a non-empty string dialogHelper.dialog->setDetailedText(nonEmptyString); QCOMPARE(dialogHelper.dialog->detailedText(), nonEmptyString); QCOMPARE(detailedTextSpy.size(), 1); QCOMPARE(detailedTextArea->text(), emptyString); QVERIFY(!detailedTextButton->isVisible()); dialogHelper.dialog->close(); dialogHelper.dialog->open(); QCOMPARE(dialogHelper.dialog->detailedText(), nonEmptyString); QCOMPARE(detailedTextArea->text(), nonEmptyString); QVERIFY2(detailedTextButton->isVisible(), "The 'show details' button should be visible when the detailedText property is not " "empty."); // Set the detailed text to an empty string dialogHelper.dialog->setDetailedText(emptyString); QCOMPARE(detailedTextSpy.size(), 2); QCOMPARE(dialogHelper.dialog->detailedText(), emptyString); QCOMPARE(detailedTextArea->text(), nonEmptyString); QVERIFY(detailedTextButton->isVisible()); dialogHelper.dialog->close(); dialogHelper.dialog->open(); QCOMPARE(dialogHelper.dialog->detailedText(), emptyString); QCOMPARE(detailedTextArea->text(), emptyString); QVERIFY2(!detailedTextButton->isVisible(), "The 'show details' button should be invisible when the detailedText property is an empty string"); // Change the detailed text property while the dialog is already open, should not immediately // update the dialog ui dialogHelper.dialog->setDetailedText(nonEmptyString); QCOMPARE(detailedTextSpy.size(), 3); QCOMPARE(dialogHelper.dialog->detailedText(), nonEmptyString); QCOMPARE(detailedTextArea->text(), emptyString); QVERIFY2(!detailedTextButton->isVisible(), "The 'show details' button should only become visible when the dialog is re-opened."); dialogHelper.dialog->close(); } void tst_QQuickMessageDialogImpl::resultReflectsLastStandardButtonPressed() { DialogTestHelper dialogHelper( this, "messageDialogWithButtons.qml"); QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); QVERIFY(dialogHelper.waitForWindowActive()); QVERIFY(dialogHelper.openDialog()); QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); QVERIFY(dialogHelper.waitForPopupWindowActiveAndPolished()); QSignalSpy acceptedSpy(dialogHelper.dialog, SIGNAL(accepted())); QSignalSpy rejectedSpy(dialogHelper.dialog, SIGNAL(rejected())); QSignalSpy resultChangedSpy(dialogHelper.dialog, SIGNAL(resultChanged())); auto buttonBox = dialogHelper.quickDialog->findChild("buttonBox"); QVERIFY(buttonBox); bool yesFound = false; bool noFound = false; bool discardFound = false; bool applyFound = false; int expectedNumberOfAcceptedSignals = 0; int expectedNumberOfRejectedSignals = 0; // The dialogButtonBox has different layouts depending on platform. This tries to account for all possible layouts. // If the role of a button is YesRole, AcceptRole, NoRole or RejectRole, then pressing that button should emit accepted, or rejected. // And since this is a MessageDialog, the result property should reflect the last button pressed, rather than the StandardCode. for (int i = 0; i < buttonBox->count(); ++i) { auto button = qobject_cast(buttonBox->itemAt(i)); switch (QQuickDialogPrivate::buttonRole(button)) { case QPlatformDialogHelper::YesRole: yesFound = true; expectedNumberOfAcceptedSignals++; QTest::mouseClick(dialogHelper.popupWindow(), Qt::LeftButton, Qt::NoModifier, button->mapToScene({ button->width() / 2, button->height() / 2 }).toPoint()); QTRY_VERIFY(!dialogHelper.isQuickDialogOpen()); QCOMPARE(dialogHelper.dialog->result(), QPlatformDialogHelper::StandardButton::Yes); QCOMPARE(resultChangedSpy.count(), i + 1); QVERIFY(dialogHelper.openDialog()); QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); break; case QPlatformDialogHelper::NoRole: noFound = true; expectedNumberOfRejectedSignals++; QTest::mouseClick(dialogHelper.popupWindow(), Qt::LeftButton, Qt::NoModifier, button->mapToScene({ button->width() / 2, button->height() / 2 }).toPoint()); QTRY_VERIFY(!dialogHelper.isQuickDialogOpen()); QCOMPARE(dialogHelper.dialog->result(), QPlatformDialogHelper::StandardButton::No); QCOMPARE(resultChangedSpy.count(), i + 1); QVERIFY(dialogHelper.openDialog()); QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); break; case QPlatformDialogHelper::DestructiveRole: discardFound = true; QTest::mouseClick(dialogHelper.popupWindow(), Qt::LeftButton, Qt::NoModifier, button->mapToScene({ button->width() / 2, button->height() / 2 }).toPoint()); QTRY_VERIFY(!dialogHelper.isQuickDialogOpen()); QCOMPARE(dialogHelper.dialog->result(), QPlatformDialogHelper::StandardButton::Discard); QCOMPARE(resultChangedSpy.count(), i + 1); QVERIFY(dialogHelper.openDialog()); QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); break; case QPlatformDialogHelper::ApplyRole: applyFound = true; QTest::mouseClick(dialogHelper.popupWindow(), Qt::LeftButton, Qt::NoModifier, button->mapToScene({ button->width() / 2, button->height() / 2 }).toPoint()); QTRY_VERIFY(!dialogHelper.isQuickDialogOpen()); QCOMPARE(dialogHelper.dialog->result(), QPlatformDialogHelper::StandardButton::Apply); QCOMPARE(resultChangedSpy.count(), i + 1); QVERIFY(dialogHelper.openDialog()); QTRY_VERIFY(dialogHelper.isQuickDialogOpen()); break; default: QFAIL(qPrintable(QStringLiteral("Unexpected role %1").arg(QQuickDialogPrivate::buttonRole(button)))); } } QVERIFY2(yesFound && noFound && discardFound && applyFound, "A button that was expected to be present, wasn't found when iterating over all of them."); QCOMPARE(acceptedSpy.count(), expectedNumberOfAcceptedSignals); QCOMPARE(rejectedSpy.count(), expectedNumberOfRejectedSignals); dialogHelper.dialog->close(); } void tst_QQuickMessageDialogImpl::checkModality_data() { QTest::addColumn("modality"); QTest::addColumn("expectedRootWindowChildCount"); QTest::addColumn("expectedChildWindowClickCount"); QTest::newRow("nonModal") << Qt::NonModal << 1 << 1; QTest::newRow("windowModal") << Qt::WindowModal << 0 << 1; // Verify application modal case only for the platform which supports multiple windows if (QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::MultipleWindows)) QTest::newRow("applicationModal") << Qt::ApplicationModal << 0 << 0; } void tst_QQuickMessageDialogImpl::checkModality() { QFETCH(Qt::WindowModality, modality); QFETCH(int, expectedRootWindowChildCount); QFETCH(int, expectedChildWindowClickCount); DialogTestHelper dialogHelper(this, "checkModality.qml"); QVERIFY2(dialogHelper.isWindowInitialized(), dialogHelper.failureMessage()); QVERIFY(dialogHelper.waitForWindowActive()); dialogHelper.dialog->setModality(modality); QCOMPARE(dialogHelper.dialog->modality(), modality); OPEN_QUICK_DIALOG(); QVERIFY(dialogHelper.waitForPopupWindowActiveAndPolished()); auto *childWindow = dialogHelper.window()->property("childWindow").value(); QVERIFY(childWindow); // Setting the child window transient parent as root window won't allow it to receive mouse // events, causing WindowModal case not to be tested. Thus, resetting the transient parent. if (modality == Qt::WindowModal) childWindow->setTransientParent(nullptr); // Platforms such as Android have system bars obscuring the content item on // top and bottom, which can lead to a mouse event being delivered to the // system bar instead of the content item. To avoid this, safe area margin // has been considered. const QMargins safeAreaMargin = dialogHelper.window()->safeAreaMargins(); const auto *rootMouseArea = dialogHelper.window()->property("rootMArea").value(); QVERIFY(rootMouseArea); QSignalSpy rmaMouseSpy(rootMouseArea, &QQuickMouseArea::clicked); QTest::mouseClick(dialogHelper.window(), Qt::LeftButton, Qt::NoModifier, QPoint(5 + safeAreaMargin.left(), 5 + safeAreaMargin.top())); QCOMPARE(rmaMouseSpy.size(), expectedRootWindowChildCount); const auto *childMouseArea = dialogHelper.window()->property("childMArea").value(); QVERIFY(childMouseArea); QSignalSpy cmaMouseSpy(childMouseArea, &QQuickMouseArea::clicked); QTest::mouseClick(childWindow, Qt::LeftButton, Qt::NoModifier, QPoint(5 + safeAreaMargin.left(), 5 + safeAreaMargin.top())); QCOMPARE(cmaMouseSpy.size(), expectedChildWindowClickCount); } QTEST_MAIN(tst_QQuickMessageDialogImpl) #include "tst_qquickmessagedialogimpl.moc"