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