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