Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
controlstestutils.cpp
Go to the documentation of this file.
1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
5
6#include <QtCore/qdiriterator.h>
7#include <QtTest/qsignalspy.h>
8#include <QtQml/qqmlcomponent.h>
9#include <QtQuickControls2/qquickstyle.h>
10#include <QtQuickControls2/private/qquickstyle_p.h>
11#include <QtQuickTemplates2/private/qquickabstractbutton_p.h>
12#include <QtQuickTemplates2/private/qquickapplicationwindow_p.h>
13#include <QtQuickTemplates2/private/qquickcontrol_p_p.h>
14#include <QtQuickTemplates2/private/qquickpopup_p.h>
15#include <QtQuickTemplates2/private/qquickpopupitem_p_p.h>
16#include <QtQuickTemplates2/private/qquickmenu_p_p.h>
17#include <QtQuickTemplates2/private/qquickmenuitem_p.h>
18#include <QtQuickTemplates2/private/qquickmenuitem_p_p.h>
19
20QQuickControlsTestUtils::QQuickControlsApplicationHelper::QQuickControlsApplicationHelper(QQmlDataTest *testCase,
21 const QString &testFilePath, const QVariantMap &initialProperties, const QStringList &qmlImportPaths)
22 : QQuickApplicationHelper(testCase, testFilePath, initialProperties, qmlImportPaths)
23{
24 if (ready)
25 appWindow = qobject_cast<QQuickApplicationWindow*>(cleanup.data());
26}
27
28/*!
29 \internal
30
31 If \a style is different from the current style, this function will
32 recreate the QML engine, clear type registrations and set the new style.
33
34 Returns \c true if successful or if \c style is already set.
35*/
37{
38 // If it's not the first time a style has been set and the new style is not different, do nothing.
39 if (!currentStyle.isEmpty() && style == currentStyle)
40 return true;
41
42 engine.reset();
43 currentStyle = style;
44 qmlClearTypeRegistrations();
45 engine.reset(new QQmlEngine);
46 QQuickStyle::setStyle(style);
47
48 QQmlComponent component(engine.data());
49 component.setData(QString::fromUtf8("import QtQuick\nimport QtQuick.Controls\n Control { }").toUtf8(), QUrl());
50 if (!component.isReady())
51 qWarning() << "Failed to load component:" << component.errorString();
52 return component.isReady();
53}
54
55void QQuickControlsTestUtils::forEachControl(QQmlEngine *engine, const QString &qqc2ImportPath,
56 const QString &styleName, const QString &targetPath, const QStringList &skipList,
58{
59 // We cannot use QQmlComponent to load QML files directly from the source tree.
60 // For styles that use internal QML types (eg. material/Ripple.qml), the source
61 // dir would be added as an "implicit" import path overriding the actual import
62 // path (qtbase/qml/QtQuick/Controls.2/Material). => The QML engine fails to load
63 // the style C++ plugin from the implicit import path (the source dir).
64 //
65 // Therefore we only use the source tree for finding out the set of QML files that
66 // a particular style implements, and then we locate the respective QML files in
67 // the engine's import path. This way we can use QQmlComponent to load each QML file
68 // for benchmarking.
69
70 const QFileInfoList entries = QDir(qqc2ImportPath + QLatin1Char('/') + styleName.toLower()).entryInfoList(
71 QStringList(QStringLiteral("*.qml")), QDir::Files);
72 for (const QFileInfo &entry : entries) {
73 QString name = entry.baseName();
74 if (!skipList.contains(name)) {
75 const auto importPathList = engine->importPathList();
76 for (const QString &importPath : importPathList) {
77 const QString relativePath = entry.dir().dirName() + QLatin1Char('/') + entry.fileName();
78 QString filePath = importPath + QLatin1Char('/') + targetPath + QLatin1Char('/') + entry.fileName();
79 if (filePath.startsWith(QLatin1Char(':')))
80 filePath.prepend(QStringLiteral("qrc"));
81 if (QFile::exists(filePath)) {
82 callback(styleName, name, relativePath, QUrl::fromLocalFile(filePath));
83 break;
84 } else {
85 QUrl url(filePath);
86 filePath = QQmlFile::urlToLocalFileOrQrc(filePath);
87 if (!filePath.isEmpty() && QFile::exists(filePath)) {
88 callback(styleName, name, relativePath, url);
89 break;
90 }
91 }
92 }
93 }
94 }
95}
96
97void QQuickControlsTestUtils::addTestRowForEachControl(QQmlEngine *engine, const QString &qqc2ImportPath,
98 const QString &styleName, const QString &targetPath, const QStringList &skipList)
99{
100 forEachControl(engine, qqc2ImportPath, styleName, targetPath, skipList, [&](
101 const QString &, const QString &, const QString &relativePath, const QUrl &absoluteUrl) {
102 QTest::newRow(qPrintable(relativePath)) << absoluteUrl;
103 });
104}
105
106
107bool QQuickControlsTestUtils::verifyButtonClickable(QQuickAbstractButton *button)
108{
109 if (!button->window()) {
110 qWarning() << "button" << button << "doesn't have an associated window";
111 return false;
112 }
113
114 if (!button->isEnabled()) {
115 qWarning() << "button" << button << "is not enabled";
116 return false;
117 }
118
119 if (!button->isVisible()) {
120 qWarning() << "button" << button << "is not visible";
121 return false;
122 }
123
124 if (button->width() <= 0.0) {
125 qWarning() << "button" << button << "must have a width greater than 0";
126 return false;
127 }
128
129 if (button->height() <= 0.0) {
130 qWarning() << "button" << button << "must have a height greater than 0";
131 return false;
132 }
133
134 return true;
135}
136
137bool QQuickControlsTestUtils::clickButton(QQuickAbstractButton *button)
138{
139 if (!verifyButtonClickable(button))
140 return false;
141
142 QSignalSpy spy(button, &QQuickAbstractButton::clicked);
143 if (!spy.isValid()) {
144 qWarning() << "Button" << button << "must have a valid clicked signal";
145 return false;
146 }
147
148 const QPoint buttonCenter = button->mapToScene(QPointF(button->width() / 2, button->height() / 2)).toPoint();
149 QTest::mouseClick(button->window(), Qt::LeftButton, Qt::NoModifier, buttonCenter);
150 if (spy.size() != 1) {
151 QDebug warning(QtWarningMsg);
152 warning.nospace() << "The clicked signal of button " << button << " was not emitted after "
153 << "clicking at " << buttonCenter << ".";
154 const QQuickPopup *popup = popupParent(button);
155 if (popup && !popup->isOpened()) {
156 warning << " The popup it's in (" << popup << ") is no longer opened; "
157 << "the click may have missed the button and gone outside of the popup, "
158 << "causing it to close.";
159 }
160 return false;
161 }
162
163 return true;
164}
165
166/*!
167 \internal
168
169 If \a menuItem is not in a menu, use \l clickButton.
170*/
171bool QQuickControlsTestUtils::clickMenuItem(QQuickMenuItem *menuItem)
172{
173 auto *menuItemPrivate = QQuickMenuItemPrivate::get(menuItem);
174 if (!menuItemPrivate->menu) {
175 qWarning() << "MenuItem" << menuItem << "must be in a menu in order to be clicked";
176 return false;
177 }
178
179 if (menuItemPrivate->menu->enter()) {
180 /*
181 FluentWinUI3 animates its height in its enter transition. This causes issues in
182 context menu tests (tst_QQuickContextMenu) on Ubuntu (X11), because the native resize
183 events caused by the menu's height changes arrive too late, causing clicks to miss the
184 menu item and instead close the menu (which clickButton now warns about).
185
186 There doesn't appear to be a way to reliably detect and hence wait for these events.
187 We also can't disable the enter transition because the menu doesn't exist until the
188 right click event, by which point the transition has also already started.
189
190 We tried an environment variable to allow the test to disable them before they start,
191 but it was still flaky. So we now simply click the menu item programmatically for menus
192 with enter transitions.
193 */
194 menuItem->click();
195 return true;
196 }
197
198 return clickButton(menuItem);
199}
200
201bool QQuickControlsTestUtils::doubleClickButton(QQuickAbstractButton *button)
202{
203 if (!verifyButtonClickable(button))
204 return false;
205
206 QSignalSpy spy(button, &QQuickAbstractButton::clicked);
207 if (!spy.isValid()) {
208 qWarning() << "button" << button << "must have a valid doubleClicked signal";
209 return false;
210 }
211
212 const QPoint buttonCenter = button->mapToScene(QPointF(button->width() / 2, button->height() / 2)).toPoint();
213 QTest::mouseDClick(button->window(), Qt::LeftButton, Qt::NoModifier, buttonCenter);
214 if (spy.size() != 1) {
215 qWarning() << "doubleClicked signal of button" << button << "was not emitted after double-clicking";
216 return false;
217 }
218
219 return true;
220}
221
222/*!
223 Allows creating QQmlComponents in C++, which is useful for tests that need
224 to check if items created from the component have the correct QML context.
225*/
226Q_INVOKABLE QQmlComponent *QQuickControlsTestUtils::ComponentCreator::createComponent(const QByteArray &data)
227{
228 std::unique_ptr<QQmlComponent> component(new QQmlComponent(qmlEngine(this)));
229 component->setData(data, QUrl());
230 if (component->isError())
231 qmlWarning(this) << "Failed to create component from the following data:\n" << data;
232 return component.release();
233}
234
236{
238}
239
241{
242 static std::unique_ptr<StyleInfo> instance(new StyleInfo);
243 // If this API was only used from QML, we could use the default JavaScriptOwnership
244 // and let the engine take ownership of us. However, it's possible to use this API
245 // only from C++, which means we need to take ownership just in case.
246 QJSEngine::setObjectOwnership(instance.get(), QJSEngine::CppOwnership);
247 return instance.get();
248}
249
250void QQuickControlsTestUtils::StyleInfo::initialize(const QString &controlsImportPath)
251{
252#if defined(Q_OS_ANDROID)
253 qWarning() << "StyleInfo is not supported when cross-compiling: QTBUG-100191";
254 return;
255#endif
256
257 QQmlEngine engine;
258 QQmlComponent component(&engine);
259 component.setData(QString::fromLatin1(
260 "import QtQuick.Templates; Control { }").toUtf8(), QUrl());
261
262 const QStringList qmlTypeNames = QQmlMetaType::qmlTypeNames();
263
264 // Collect the files from each style in the source tree.
265 QDirIterator it(controlsImportPath, QStringList() << QLatin1String("*.qml") << QLatin1String("*.js"),
266 QDir::Files, QDirIterator::Subdirectories);
267 while (it.hasNext()) {
268 it.next();
269 QFileInfo info = it.fileInfo();
270 if (qmlTypeNames.contains(QLatin1String("QtQuick.Templates/") + info.baseName())) {
271 const auto dirName = info.dir().dirName();
272 const auto typeName = info.fileName();
273 m_sourceQmlFiles.append({dirName, typeName, dirName + u"/" + typeName, info.filePath() });
274 }
275 }
276
277 // Gather the list of styles.
278 QStringList builtInStyles = QQuickStylePrivate::builtInStyles();
279 // TODO: add native styles: QTBUG-87108. Originally this list was hard-coded
280 // and didn't include them.
281 const QStringList nativeStyles = { QLatin1String("macOS"), QLatin1String("Windows") };
282 builtInStyles.removeIf([&nativeStyles](const QString &styleName){
283 return nativeStyles.contains(styleName);
284 });
285
286 QList<std::pair<QString, QString>> styleRelativePaths;
287 for (const auto &styleName : std::as_const(builtInStyles)) {
288 // E.g. { "Basic", "QtQuick/Controls/Basic" }
289 styleRelativePaths.append(std::make_pair(styleName, QLatin1String("QtQuick/Controls/") + styleName));
290 }
291
292 // Then, collect the files from each installed style directory.
293 for (const auto &stylePathPair : styleRelativePaths) {
294 forEachControl(&engine, controlsImportPath, stylePathPair.first, stylePathPair.second, QStringList(),
295 [&](const QString &styleName, const QString &typeName, const QString &relativePath,
296 const QUrl &absoluteUrl) {
297 m_installedQmlFiles.append({ styleName, typeName, relativePath, absoluteUrl.toLocalFile() });
298 });
299 }
300
301 std::sort(m_sourceQmlFiles.begin(), m_sourceQmlFiles.end());
302 std::sort(m_installedQmlFiles.begin(), m_installedQmlFiles.end());
303}
304
306{
307 return QQuickStyle::name();
308}
309
310void QQuickControlsTestUtils::StyleInfo::warnIfNotInitialized() const
311{
312 if (m_sourceQmlFiles.isEmpty())
313 qWarning() << "StyleInfo hasn't been initialized";
314}
315
318{
319 return relativePath < rhs.relativePath;
320}
321
323{
324 warnIfNotInitialized();
325 return m_sourceQmlFiles;
326}
327
329{
330 warnIfNotInitialized();
331 return m_installedQmlFiles;
332}
333
334/*!
335 It's recommended to use try-finally (see tst_monthgrid.qml for an example)
336 or init/initTestCase and cleanup/cleanupTestCase if setting environment
337 variables, in order to restore previous values.
338*/
339QString QQuickControlsTestUtils::SystemEnvironment::value(const QString &name)
340{
341 return QString::fromLocal8Bit(qgetenv(name.toLocal8Bit()));
342}
343
344bool QQuickControlsTestUtils::SystemEnvironment::setValue(const QString &name, const QString &value)
345{
346 return qputenv(name.toLocal8Bit(), value.toLocal8Bit());
347}
348
350{
351 QString message;
352 QDebug debug(&message);
353 const auto *controlPrivate = QQuickControlPrivate::get(control);
354 const QQuickWindow *window = control->window();
355 const QString activeFocusItemStr = window
356 ? QDebug::toString(window->activeFocusItem()) : QStringLiteral("(unknown; control has no window)");
357 debug.nospace() << "control: " << control << " activeFocus: " << control->hasActiveFocus()
358 << " focusReason: " << static_cast<Qt::FocusReason>(controlPrivate->focusReason)
359 << " activeFocusItem: " << activeFocusItemStr;
360 return message;
361}
362
363bool QQuickControlsTestUtils::ApplicationAttributes::test(Qt::ApplicationAttribute attribute) const
364{
365 return QCoreApplication::testAttribute(attribute);
366}
367
368void QQuickControlsTestUtils::ApplicationAttributes::set(Qt::ApplicationAttribute attribute, bool on)
369{
370 QCoreApplication::setAttribute(attribute, on);
371}
372
374{
375#if defined(Q_OS_WINDOWS) || defined(Q_OS_MACOS)
376 return QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::Capability::MultipleWindows);
377#else
378 return false;
379#endif
380}
381
382/*!
383 \internal
384
385 Finds the popup that \a item is in, or returns \c nullptr.
386*/
387QQuickPopup *QQuickControlsTestUtils::popupParent(QQuickItem *item)
388{
389 QQuickItem *parentItem = item;
390 while (parentItem) {
391 auto *parentAsPopupItem = qobject_cast<QQuickPopupItem *>(parentItem);
392 if (parentAsPopupItem)
393 return QQuickPopupItemPrivate::get(parentAsPopupItem)->popup;
394
395 parentItem = parentItem->parentItem();
396 }
397
398 return nullptr;
399}
400
402{
403 QByteArray message;
404 QDebug debug(&message);
405 const QQuickWindow *window = popup->window();
406 const QString activeFocusItemStr = window
407 ? QDebug::toString(window->activeFocusItem()) : QStringLiteral("(unknown; popup has no window)");
408 debug.nospace() << "popup: " << popup;
409 debug.noquote() << " window's activeFocusItem: " << activeFocusItemStr;
410 return message;
411}
The QQmlComponent class encapsulates a QML component definition.
Q_INVOKABLE void set(Qt::ApplicationAttribute attribute, bool on=true)
QList< QmlFileData > installedQmlFiles() const
QList< QmlFileData > sourceQmlFiles() const
Q_INVOKABLE bool setValue(const QString &name, const QString &value)
bool verifyButtonClickable(QQuickAbstractButton *button)
void forEachControl(QQmlEngine *engine, const QString &qqc2ImportPath, const QString &styleName, const QString &targetPath, const QStringList &skipList, ForEachCallback callback)
void addTestRowForEachControl(QQmlEngine *engine, const QString &qqc2ImportPath, const QString &styleName, const QString &targetPath, const QStringList &skipList=QStringList())
bool clickMenuItem(QQuickMenuItem *menuItem)
std::function< void(const QString &, const QString &, const QString &, const QUrl &)> ForEachCallback
bool doubleClickButton(QQuickAbstractButton *button)
QQuickPopup * popupParent(QQuickItem *item)
QString visualFocusFailureMessage(QQuickControl *control)
bool clickButton(QQuickAbstractButton *button)
QByteArray qActiveFocusFailureMessage(QQuickPopup *popup)