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