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
visualtestutils.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/QCoreApplication>
7#include <QtCore/qloggingcategory.h>
8#include <QtCore/private/qabstractanimation_p.h>
9#include <QtCore/private/qvariantanimation_p.h>
10#include <QtCore/QDebug>
11#include <QtQuick/QQuickItem>
12#include <QtQml/qqmlcomponent.h>
13#if QT_CONFIG(quick_itemview)
14#include <QtQuick/private/qquickitemview_p.h>
15#endif
16#include <QtQuickTest/QtQuickTest>
17#include <QtQuickTestUtils/private/viewtestutils_p.h>
18
20
21Q_STATIC_LOGGING_CATEGORY(lcCompareImages, "qt.quicktestutils.compareimages")
22
23QQuickItem *QQuickVisualTestUtils::findVisibleChild(QQuickItem *parent, const QString &objectName)
24{
25 QQuickItem *item = nullptr;
26 QList<QQuickItem*> items = parent->findChildren<QQuickItem*>(objectName);
27 for (int i = 0; i < items.size(); ++i) {
28 if (items.at(i)->isVisible() && !QQuickItemPrivate::get(items.at(i))->culled) {
29 item = items.at(i);
30 break;
31 }
32 }
33 return item;
34}
35
36void QQuickVisualTestUtils::dumpTree(QQuickItem *parent, int depth)
37{
38 static QString padding = QStringLiteral(" ");
39 for (int i = 0; i < parent->childItems().size(); ++i) {
40 QQuickItem *item = qobject_cast<QQuickItem*>(parent->childItems().at(i));
41 if (!item)
42 continue;
43 qDebug() << padding.left(depth*2) << item;
44 dumpTree(item, depth+1);
45 }
46}
47
48void QQuickVisualTestUtils::moveMouseAway(QQuickWindow *window)
49{
50#if QT_CONFIG(cursor) // Get the cursor out of the way.
51 // Using "bottomRight() + QPoint(100, 100)" was causing issues on Ubuntu,
52 // where the window was positioned at the bottom right corner of the window
53 // (even after centering the window on the screen), so we use another position.
54 QCursor::setPos(window->frameGeometry().bottomLeft() + QPoint(-10, 10));
55#endif
56
57 // make sure hover events from QQuickDeliveryAgentPrivate::flushFrameSynchronousEvents()
58 // do not interfere with the tests
59 QEvent leave(QEvent::Leave);
60 QCoreApplication::sendEvent(window, &leave);
61}
62
63void QQuickVisualTestUtils::centerOnScreen(QQuickWindow *window)
64{
65 QQuickViewTestUtils::centerOnScreen(window);
66}
67
68QPoint QQuickVisualTestUtils::lerpPoints(const QPoint &point1, const QPoint &point2, qreal t)
69{
70 return QPoint(_q_interpolate(point1.x(), point2.x(), t), _q_interpolate(point1.y(), point2.y(), t));
71};
72
73/*!
74 \internal
75
76 Convenience class to linearly interpolate between two pointer move points.
77
78 \code
79 PointLerper pointLerper(window);
80 // Lerps from {0, 0} to {15, 15}.
81 pointLerper.move(15, 15);
82 QVERIFY(parentButton->isHovered());
83
84 // Lerps from {15, 15} to {25, 25}.
85 pointLerper.move(25, 25);
86 QVERIFY(childButton->isHovered());
87 \endcode
88*/
89QQuickVisualTestUtils::PointLerper::PointLerper(QQuickWindow *window, const QPoint &startingPosition,
90 const QPointingDevice *pointingDevice)
91 : mWindow(window)
94{
95}
96
97/*!
98 \internal
99
100 Moves from the last pos (or {0, 0} if there have been no calls
101 to this function yet) to \a pos using linear interpolation
102 over 10 (default value) steps with 1 ms (default value) delays
103 between each step.
104*/
105void QQuickVisualTestUtils::PointLerper::move(const QPoint &pos, int steps, int delayInMilliseconds)
106{
107 forEachStep(steps, [&](qreal progress) {
108 QQuickTest::pointerMove(mPointingDevice, mWindow, 0, lerpPoints(mFrom, pos, progress));
109 QTest::qWait(delayInMilliseconds);
110 });
111 mFrom = pos;
112};
113
114void QQuickVisualTestUtils::PointLerper::move(int x, int y, int steps, int delayInMilliseconds)
115{
116 move(QPoint(x, y), steps, delayInMilliseconds);
117};
118
119/*!
120 \internal
121
122 Returns \c true if \c {item->isVisible()} returns \c true, and
123 the item is not culled.
124*/
126{
127 return item->isVisible() && !QQuickItemPrivate::get(item)->culled;
128}
129
130/*!
131 \internal
132
133 Compares \a ia with \a ib, returning \c true if the images are equal.
134 If they are not equal, \c false is returned and \a errorMessage is set.
135
136 A custom compare function to avoid issues such as:
137 When running on native Nvidia graphics cards on linux, the
138 distance field glyph pixels have a measurable, but not visible
139 pixel error. This was GT-216 with the ubuntu "nvidia-319" driver package.
140 llvmpipe does not show the same issue.
141
142 To see the actual and expected images upon failure, enable the
143 \c qt.quicktestutils.compareimages debug logging category.
144*/
145bool QQuickVisualTestUtils::compareImages(const QImage &ia, const QImage &ib, QString *errorMessage)
146{
147 auto maybeSaveImagesForDebugging = [ia, ib](QDebug &debug) {
148 if (!lcCompareImages().isDebugEnabled())
149 return;
150
151 const QDir saveDir(QCoreApplication::applicationDirPath());
152 QString imageFileNamePrefix = QString::fromUtf8("%1-%2").arg(
153 QString::fromUtf8(QTest::currentAppName()),
154 QString::fromUtf8(QTest::currentTestFunction()));
155 if (QTest::currentDataTag())
156 imageFileNamePrefix.append(QStringLiteral("-") + QString::fromUtf8(QTest::currentDataTag()));
157
158 const QString actualImageFilePath = saveDir.filePath(imageFileNamePrefix + QLatin1String("-actual.png"));
159 const bool actualImageSaved = ia.save(actualImageFilePath);
160 if (!actualImageSaved)
161 qWarning() << "Failed to save actual image to" << actualImageFilePath;
162
163 const QString expectedImageFilePath = saveDir.filePath(imageFileNamePrefix + QLatin1String("-expected.png"));
164 const bool expectedImageSaved = ib.save(expectedImageFilePath);
165 if (!expectedImageSaved)
166 qWarning() << "Failed to save expected image to" << expectedImageFilePath;
167
168 if (actualImageSaved && expectedImageSaved) {
169 debug.noquote() << "\nActual image saved to:" << actualImageFilePath;
170 debug << "\nExpected image saved to:" << expectedImageFilePath;
171 }
172 };
173
174 if (ia.size() != ib.size()) {
175 QDebug debug(errorMessage);
176 debug << "Images are of different size:" << ia.size() << ib.size()
177 << "DPR:" << ia.devicePixelRatio() << ib.devicePixelRatio();
178 maybeSaveImagesForDebugging(debug);
179 return false;
180 }
181 if (ia.format() != ib.format()) {
182 QDebug debug(errorMessage);
183 debug << "Images are of different formats:" << ia.format() << ib.format();
184 maybeSaveImagesForDebugging(debug);
185 return false;
186 }
187 if (ia.depth() != 32) {
188 QDebug debug(errorMessage);
189 debug << "This function only supports bit depths of 32 - depth of images is:" << ia.depth();
190 maybeSaveImagesForDebugging(debug);
191 return false;
192 }
193
194 int w = ia.width();
195 int h = ia.height();
196 const int tolerance = 5;
197 for (int y=0; y<h; ++y) {
198 const uint *as= (const uint *) ia.constScanLine(y);
199 const uint *bs= (const uint *) ib.constScanLine(y);
200 for (int x=0; x<w; ++x) {
201 uint a = as[x];
202 uint b = bs[x];
203
204 // No tolerance for error in the alpha.
205 if ((a & 0xff000000) != (b & 0xff000000)
206 || qAbs(qRed(a) - qRed(b)) > tolerance
207 || qAbs(qGreen(a) - qGreen(b)) > tolerance
208 || qAbs(qBlue(a) - qBlue(b)) > tolerance) {
209 QDebug debug(errorMessage);
210 debug << "Mismatch at:" << x << y << ':'
211 << Qt::hex << Qt::showbase << a << b;
212 maybeSaveImagesForDebugging(debug);
213 return false;
214 }
215 }
216 }
217 return true;
218}
219
220#if QT_CONFIG(quick_itemview)
221/*!
222 \internal
223
224 Finds the delegate at \c index belonging to \c itemView, using the given \c flags.
225
226 If the view needs to be polished, the function will wait for it to be done before continuing,
227 and returns \c nullptr if the polish didn't happen.
228*/
229QQuickItem *QQuickVisualTestUtils::findViewDelegateItem(QQuickItemView *itemView, int index, FindViewDelegateItemFlags flags)
230{
231 if (QQuickTest::qIsPolishScheduled(itemView)) {
232 if (!QQuickTest::qWaitForPolish(itemView)) {
233 qWarning() << "failed to polish" << itemView;
234 return nullptr;
235 }
236 }
237
238 // Do this after the polish, just in case the count changes after a polish...
239 if (index <= -1 || index >= itemView->count()) {
240 qWarning() << "index" << index << "is out of bounds for" << itemView;
241 return nullptr;
242 }
243
244 if (flags.testFlag(FindViewDelegateItemFlag::PositionViewAtIndex))
245 itemView->positionViewAtIndex(index, QQuickItemView::Center);
246
247 return itemView->itemAtIndex(index);
248}
249#endif
250
251QQuickVisualTestUtils::QQuickApplicationHelper::QQuickApplicationHelper(QQmlDataTest *testCase,
252 const QString &testFilePath, const QVariantMap &initialProperties, const QStringList &qmlImportPaths)
253{
254 for (const auto &path : qmlImportPaths)
255 engine.addImportPath(path);
256
257 QQmlComponent component(&engine);
258
259 component.loadUrl(testCase->testFileUrl(testFilePath));
260 QVERIFY2(component.isReady(), qPrintable(component.errorString()));
261 QObject *rootObject = component.createWithInitialProperties(initialProperties);
262 cleanup.reset(rootObject);
263 if (component.isError() || !rootObject) {
264 errorMessage = QString::fromUtf8("Failed to create window: %1").arg(component.errorString()).toUtf8();
265 return;
266 }
267
268 window = qobject_cast<QQuickWindow*>(rootObject);
269 if (!window) {
270 errorMessage = QString::fromUtf8("Root object %1 must be a QQuickWindow subclass").arg(QDebug::toString(window)).toUtf8();
271 return;
272 }
273
274 if (window->isVisible()) {
275 errorMessage = QString::fromUtf8("Expected window not to be visible, but it is").toUtf8();
276 return;
277 }
278
279 ready = true;
280}
281
282QQuickVisualTestUtils::MnemonicKeySimulator::MnemonicKeySimulator(QWindow *window)
283 : m_window(window), m_modifiers(Qt::NoModifier)
284{
285}
286
288{
289 // QTest::keyPress() but not generating the press event for the modifier key.
290 if (key == Qt::Key_Alt)
291 m_modifiers |= Qt::AltModifier;
292 QTest::simulateEvent(m_window, true, key, m_modifiers, QString(), false);
293}
294
296{
297 // QTest::keyRelease() but not generating the release event for the modifier key.
298 if (key == Qt::Key_Alt)
299 m_modifiers &= ~Qt::AltModifier;
300 QTest::simulateEvent(m_window, false, key, m_modifiers, QString(), false);
301}
302
304{
305 press(key);
306 release(key);
307}
308
310{
311 return item->mapToScene(QPointF(item->width() / 2, item->height() / 2)).toPoint();
312}
313
314QPoint QQuickVisualTestUtils::mapToWindow(const QQuickItem *item, qreal relativeX, qreal relativeY)
315{
316 return item->mapToScene(QPointF(relativeX, relativeY)).toPoint();
317}
318
319QPoint QQuickVisualTestUtils::mapToWindow(const QQuickItem *item, const QPointF &relativePos)
320{
321 return mapToWindow(item, relativePos.x(), relativePos.y());
322}
323
324void QQuickVisualTestUtils::setFastAnimations(bool fastAnimations)
325{
326 QUnifiedTimer::instance()->setSpeedModifier(fastAnimations ? 5 : 1);
327}
328
329QT_END_NAMESPACE
330
331#include "moc_visualtestutils_p.cpp"
void move(const QPoint &pos, int steps=10, int delayInMilliseconds=1)
void move(int x, int y, int steps=10, int delayInMilliseconds=1)
PointLerper(QQuickWindow *window, const QPoint &startingPosition=QPoint(0, 0), const QPointingDevice *pointingDevice=QPointingDevice::primaryPointingDevice())
void centerOnScreen(QQuickWindow *window)
void dumpTree(QQuickItem *parent, int depth=0)
bool isDelegateVisible(QQuickItem *item)
QPoint mapToWindow(const QQuickItem *item, qreal relativeX, qreal relativeY)
QPoint mapCenterToWindow(const QQuickItem *item)
void setFastAnimations(bool fastAnimations)
QPoint mapToWindow(const QQuickItem *item, const QPointF &relativePos)
void moveMouseAway(QQuickWindow *window)
bool compareImages(const QImage &ia, const QImage &ib, QString *errorMessage)
QPoint lerpPoints(const QPoint &point1, const QPoint &point2, qreal t)
Combined button and popup list for selecting options.