| 1 | // Copyright (C) 2021 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
| 3 | |
| 4 | #include "controlstestutils_p.h" |
| 5 | |
| 6 | #include <QtTest/qsignalspy.h> |
| 7 | #include <QtQml/qqmlcomponent.h> |
| 8 | #include <QtQuickControls2/qquickstyle.h> |
| 9 | #include <QtQuickTemplates2/private/qquickabstractbutton_p.h> |
| 10 | #include <QtQuickTemplates2/private/qquickapplicationwindow_p.h> |
| 11 | #include <QtQuickTemplates2/private/qquickcontrol_p_p.h> |
| 12 | |
| 13 | QQuickControlsTestUtils::QQuickControlsApplicationHelper::QQuickControlsApplicationHelper(QQmlDataTest *testCase, |
| 14 | const QString &testFilePath, const QVariantMap &initialProperties, const QStringList &qmlImportPaths) |
| 15 | : QQuickApplicationHelper(testCase, testFilePath, initialProperties, qmlImportPaths) |
| 16 | { |
| 17 | if (ready) |
| 18 | appWindow = qobject_cast<QQuickApplicationWindow*>(object: cleanup.data()); |
| 19 | } |
| 20 | |
| 21 | /*! |
| 22 | \internal |
| 23 | |
| 24 | If \a style is different from the current style, this function will |
| 25 | recreate the QML engine, clear type registrations and set the new style. |
| 26 | |
| 27 | Returns \c true if successful or if \c style is already set. |
| 28 | */ |
| 29 | bool QQuickControlsTestUtils::QQuickStyleHelper::updateStyle(const QString &style) |
| 30 | { |
| 31 | // If it's not the first time a style has been set and the new style is not different, do nothing. |
| 32 | if (!currentStyle.isEmpty() && style == currentStyle) |
| 33 | return true; |
| 34 | |
| 35 | engine.reset(); |
| 36 | currentStyle = style; |
| 37 | qmlClearTypeRegistrations(); |
| 38 | engine.reset(other: new QQmlEngine); |
| 39 | QQuickStyle::setStyle(style); |
| 40 | |
| 41 | QQmlComponent component(engine.data()); |
| 42 | component.setData(QString::fromUtf8(utf8: "import QtQuick\nimport QtQuick.Controls\n Control { }" ).toUtf8(), baseUrl: QUrl()); |
| 43 | if (!component.isReady()) |
| 44 | qWarning() << "Failed to load component:" << component.errorString(); |
| 45 | return component.isReady(); |
| 46 | } |
| 47 | |
| 48 | void QQuickControlsTestUtils::forEachControl(QQmlEngine *engine, const QString &qqc2ImportPath, |
| 49 | const QString &sourcePath, const QString &targetPath, const QStringList &skipList, |
| 50 | QQuickControlsTestUtils::ForEachCallback callback) |
| 51 | { |
| 52 | // We cannot use QQmlComponent to load QML files directly from the source tree. |
| 53 | // For styles that use internal QML types (eg. material/Ripple.qml), the source |
| 54 | // dir would be added as an "implicit" import path overriding the actual import |
| 55 | // path (qtbase/qml/QtQuick/Controls.2/Material). => The QML engine fails to load |
| 56 | // the style C++ plugin from the implicit import path (the source dir). |
| 57 | // |
| 58 | // Therefore we only use the source tree for finding out the set of QML files that |
| 59 | // a particular style implements, and then we locate the respective QML files in |
| 60 | // the engine's import path. This way we can use QQmlComponent to load each QML file |
| 61 | // for benchmarking. |
| 62 | |
| 63 | const QFileInfoList entries = QDir(qqc2ImportPath + QLatin1Char('/') + sourcePath).entryInfoList( |
| 64 | nameFilters: QStringList(QStringLiteral("*.qml" )), filters: QDir::Files); |
| 65 | for (const QFileInfo &entry : entries) { |
| 66 | QString name = entry.baseName(); |
| 67 | if (!skipList.contains(str: name)) { |
| 68 | const auto importPathList = engine->importPathList(); |
| 69 | for (const QString &importPath : importPathList) { |
| 70 | QString name = entry.dir().dirName() + QLatin1Char('/') + entry.fileName(); |
| 71 | QString filePath = importPath + QLatin1Char('/') + targetPath + QLatin1Char('/') + entry.fileName(); |
| 72 | if (filePath.startsWith(c: QLatin1Char(':'))) |
| 73 | filePath.prepend(QStringLiteral("qrc" )); |
| 74 | if (QFile::exists(fileName: filePath)) { |
| 75 | callback(name, QUrl::fromLocalFile(localfile: filePath)); |
| 76 | break; |
| 77 | } else { |
| 78 | QUrl url(filePath); |
| 79 | filePath = QQmlFile::urlToLocalFileOrQrc(filePath); |
| 80 | if (!filePath.isEmpty() && QFile::exists(fileName: filePath)) { |
| 81 | callback(name, url); |
| 82 | break; |
| 83 | } |
| 84 | } |
| 85 | } |
| 86 | } |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | void QQuickControlsTestUtils::addTestRowForEachControl(QQmlEngine *engine, const QString &qqc2ImportPath, |
| 91 | const QString &sourcePath, const QString &targetPath, const QStringList &skipList) |
| 92 | { |
| 93 | forEachControl(engine, qqc2ImportPath, sourcePath, targetPath, skipList, callback: [&](const QString &relativePath, const QUrl &absoluteUrl) { |
| 94 | QTest::newRow(qPrintable(relativePath)) << absoluteUrl; |
| 95 | }); |
| 96 | } |
| 97 | |
| 98 | bool QQuickControlsTestUtils::verifyButtonClickable(QQuickAbstractButton *button) |
| 99 | { |
| 100 | if (!button->window()) { |
| 101 | qWarning() << "button" << button << "doesn't have an associated window" ; |
| 102 | return false; |
| 103 | } |
| 104 | |
| 105 | if (!button->isEnabled()) { |
| 106 | qWarning() << "button" << button << "is not enabled" ; |
| 107 | return false; |
| 108 | } |
| 109 | |
| 110 | if (!button->isVisible()) { |
| 111 | qWarning() << "button" << button << "is not visible" ; |
| 112 | return false; |
| 113 | } |
| 114 | |
| 115 | if (button->width() <= 0.0) { |
| 116 | qWarning() << "button" << button << "must have a width greater than 0" ; |
| 117 | return false; |
| 118 | } |
| 119 | |
| 120 | if (button->height() <= 0.0) { |
| 121 | qWarning() << "button" << button << "must have a height greater than 0" ; |
| 122 | return false; |
| 123 | } |
| 124 | |
| 125 | return true; |
| 126 | } |
| 127 | |
| 128 | bool QQuickControlsTestUtils::clickButton(QQuickAbstractButton *button) |
| 129 | { |
| 130 | if (!verifyButtonClickable(button)) |
| 131 | return false; |
| 132 | |
| 133 | QSignalSpy spy(button, &QQuickAbstractButton::clicked); |
| 134 | if (!spy.isValid()) { |
| 135 | qWarning() << "button" << button << "must have a valid clicked signal" ; |
| 136 | return false; |
| 137 | } |
| 138 | |
| 139 | const QPoint buttonCenter = button->mapToScene(point: QPointF(button->width() / 2, button->height() / 2)).toPoint(); |
| 140 | QTest::mouseClick(window: button->window(), button: Qt::LeftButton, stateKey: Qt::NoModifier, pos: buttonCenter); |
| 141 | if (spy.size() != 1) { |
| 142 | qWarning() << "clicked signal of button" << button << "was not emitted after clicking" ; |
| 143 | return false; |
| 144 | } |
| 145 | |
| 146 | return true; |
| 147 | } |
| 148 | |
| 149 | bool QQuickControlsTestUtils::doubleClickButton(QQuickAbstractButton *button) |
| 150 | { |
| 151 | if (!verifyButtonClickable(button)) |
| 152 | return false; |
| 153 | |
| 154 | QSignalSpy spy(button, &QQuickAbstractButton::clicked); |
| 155 | if (!spy.isValid()) { |
| 156 | qWarning() << "button" << button << "must have a valid doubleClicked signal" ; |
| 157 | return false; |
| 158 | } |
| 159 | |
| 160 | const QPoint buttonCenter = button->mapToScene(point: QPointF(button->width() / 2, button->height() / 2)).toPoint(); |
| 161 | QTest::mouseDClick(window: button->window(), button: Qt::LeftButton, stateKey: Qt::NoModifier, pos: buttonCenter); |
| 162 | if (spy.size() != 1) { |
| 163 | qWarning() << "doubleClicked signal of button" << button << "was not emitted after double-clicking" ; |
| 164 | return false; |
| 165 | } |
| 166 | |
| 167 | return true; |
| 168 | } |
| 169 | |
| 170 | /*! |
| 171 | Allows creating QQmlComponents in C++, which is useful for tests that need |
| 172 | to check if items created from the component have the correct QML context. |
| 173 | */ |
| 174 | Q_INVOKABLE QQmlComponent *QQuickControlsTestUtils::ComponentCreator::createComponent(const QByteArray &data) |
| 175 | { |
| 176 | std::unique_ptr<QQmlComponent> component(new QQmlComponent(qmlEngine(this))); |
| 177 | component->setData(data, baseUrl: QUrl()); |
| 178 | if (component->isError()) |
| 179 | qmlWarning(me: this) << "Failed to create component from the following data:\n" << data; |
| 180 | return component.release(); |
| 181 | } |
| 182 | |
| 183 | QString QQuickControlsTestUtils::StyleInfo::styleName() const |
| 184 | { |
| 185 | return QQuickStyle::name(); |
| 186 | } |
| 187 | |
| 188 | QString QQuickControlsTestUtils::visualFocusFailureMessage(QQuickControl *control) |
| 189 | { |
| 190 | QString message; |
| 191 | QDebug debug(&message); |
| 192 | const auto *controlPrivate = QQuickControlPrivate::get(control); |
| 193 | const QQuickWindow *window = control->window(); |
| 194 | const QString activeFocusItemStr = window |
| 195 | ? QDebug::toString(object: window->activeFocusItem()) : QStringLiteral("(unknown; control has no window)" ); |
| 196 | debug.nospace() << "control: " << control << " activeFocus: " << control->hasActiveFocus() |
| 197 | << " focusReason: " << static_cast<Qt::FocusReason>(controlPrivate->focusReason) |
| 198 | << " activeFocusItem: " << activeFocusItemStr; |
| 199 | return message; |
| 200 | } |
| 201 | |