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