1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include "viewtestutils_p.h"
5
6#include <QtCore/QRandomGenerator>
7#include <QtCore/QTimer>
8#include <QtQuick/QQuickView>
9#include <QtQuick/QQuickView>
10#include <QtGui/QScreen>
11#include <QtGui/qpa/qwindowsysteminterface.h>
12#include <QtGui/private/qhighdpiscaling_p.h>
13
14#include <QtTest/QTest>
15
16#include <QtQuick/private/qquickdeliveryagent_p_p.h>
17#if QT_CONFIG(quick_itemview)
18#include <QtQuick/private/qquickitemview_p_p.h>
19#endif
20#include <QtQuick/private/qquickwindow_p.h>
21
22#include <QtQuickTestUtils/private/visualtestutils_p.h>
23
24QT_BEGIN_NAMESPACE
25
26QQuickView *QQuickViewTestUtils::createView()
27{
28 QQuickView *window = new QQuickView(0);
29 const QSize size(240, 320);
30 window->resize(newSize: size);
31 QQuickViewTestUtils::centerOnScreen(window, size);
32 return window;
33}
34
35void QQuickViewTestUtils::centerOnScreen(QQuickWindow *window, const QSize &size)
36{
37 const QRect screenGeometry = window->screen()->availableGeometry();
38 const QPoint offset = QPoint(size.width() / 2, size.height() / 2);
39 window->setFramePosition(screenGeometry.center() - offset);
40}
41
42void QQuickViewTestUtils::centerOnScreen(QQuickWindow *window)
43{
44 QQuickViewTestUtils::centerOnScreen(window, size: window->size());
45}
46
47void QQuickViewTestUtils::moveMouseAway(QQuickView *window)
48{
49#if QT_CONFIG(cursor) // Get the cursor out of the way.
50 QCursor::setPos(window->geometry().topRight() + QPoint(100, 100));
51#else
52 Q_UNUSED(window);
53#endif
54}
55
56QList<int> QQuickViewTestUtils::adjustIndexesForAddDisplaced(const QList<int> &indexes, int index, int count)
57{
58 QList<int> result;
59 for (int i=0; i<indexes.size(); i++) {
60 int num = indexes[i];
61 if (num >= index) {
62 num += count;
63 }
64 result << num;
65 }
66 return result;
67}
68
69QList<int> QQuickViewTestUtils::adjustIndexesForMove(const QList<int> &indexes, int from, int to, int count)
70{
71 QList<int> result;
72 for (int i=0; i<indexes.size(); i++) {
73 int num = indexes[i];
74 if (from < to) {
75 if (num >= from && num < from + count)
76 num += (to - from); // target
77 else if (num >= from && num < to + count)
78 num -= count; // displaced
79 } else if (from > to) {
80 if (num >= from && num < from + count)
81 num -= (from - to); // target
82 else if (num >= to && num < from + count)
83 num += count; // displaced
84 }
85 result << num;
86 }
87 return result;
88}
89
90QList<int> QQuickViewTestUtils::adjustIndexesForRemoveDisplaced(const QList<int> &indexes, int index, int count)
91{
92 QList<int> result;
93 for (int i=0; i<indexes.size(); i++) {
94 int num = indexes[i];
95 if (num >= index)
96 num -= count;
97 result << num;
98 }
99 return result;
100}
101
102QQuickViewTestUtils::QaimModel::QaimModel(QObject *parent)
103 : QAbstractListModel(parent)
104{
105}
106
107int QQuickViewTestUtils::QaimModel::rowCount(const QModelIndex &parent) const
108{
109 Q_UNUSED(parent);
110 return list.size();
111}
112
113int QQuickViewTestUtils::QaimModel::columnCount(const QModelIndex &parent) const
114{
115 Q_UNUSED(parent);
116 return columns;
117}
118
119QHash<int,QByteArray> QQuickViewTestUtils::QaimModel::roleNames() const
120{
121 QHash<int,QByteArray> roles = QAbstractListModel::roleNames();
122 roles.insert(key: Name, value: "name");
123 roles.insert(key: Number, value: "number");
124 return roles;
125}
126
127QVariant QQuickViewTestUtils::QaimModel::data(const QModelIndex &index, int role) const
128{
129 QVariant rv;
130 if (role == Name)
131 rv = list.at(i: index.row()).first;
132 else if (role == Number)
133 rv = list.at(i: index.row()).second;
134
135 return rv;
136}
137
138int QQuickViewTestUtils::QaimModel::count() const
139{
140 return rowCount() * columnCount();
141}
142
143QString QQuickViewTestUtils::QaimModel::name(int index) const
144{
145 return list.at(i: index).first;
146}
147
148QString QQuickViewTestUtils::QaimModel::number(int index) const
149{
150 return list.at(i: index).second;
151}
152
153void QQuickViewTestUtils::QaimModel::addItem(const QString &name, const QString &number)
154{
155 emit beginInsertRows(parent: QModelIndex(), first: list.size(), last: list.size());
156 list.append(t: QPair<QString,QString>(name, number));
157 emit endInsertRows();
158}
159
160void QQuickViewTestUtils::QaimModel::addItems(const QList<QPair<QString, QString> > &items)
161{
162 emit beginInsertRows(parent: QModelIndex(), first: list.size(), last: list.size()+items.size()-1);
163 for (int i=0; i<items.size(); i++)
164 list.append(t: QPair<QString,QString>(items[i].first, items[i].second));
165 emit endInsertRows();
166}
167
168void QQuickViewTestUtils::QaimModel::insertItem(int index, const QString &name, const QString &number)
169{
170 emit beginInsertRows(parent: QModelIndex(), first: index, last: index);
171 list.insert(i: index, t: QPair<QString,QString>(name, number));
172 emit endInsertRows();
173}
174
175void QQuickViewTestUtils::QaimModel::insertItems(int index, const QList<QPair<QString, QString> > &items)
176{
177 emit beginInsertRows(parent: QModelIndex(), first: index, last: index+items.size()-1);
178 for (int i=0; i<items.size(); i++)
179 list.insert(i: index + i, t: QPair<QString,QString>(items[i].first, items[i].second));
180 emit endInsertRows();
181}
182
183void QQuickViewTestUtils::QaimModel::removeItem(int index)
184{
185 emit beginRemoveRows(parent: QModelIndex(), first: index, last: index);
186 list.removeAt(i: index);
187 emit endRemoveRows();
188}
189
190void QQuickViewTestUtils::QaimModel::removeItems(int index, int count)
191{
192 emit beginRemoveRows(parent: QModelIndex(), first: index, last: index+count-1);
193 while (count--)
194 list.removeAt(i: index);
195 emit endRemoveRows();
196}
197
198void QQuickViewTestUtils::QaimModel::moveItem(int from, int to)
199{
200 emit beginMoveRows(sourceParent: QModelIndex(), sourceFirst: from, sourceLast: from, destinationParent: QModelIndex(), destinationRow: to);
201 list.move(from, to);
202 emit endMoveRows();
203}
204
205void QQuickViewTestUtils::QaimModel::moveItems(int from, int to, int count)
206{
207 emit beginMoveRows(sourceParent: QModelIndex(), sourceFirst: from, sourceLast: from+count-1, destinationParent: QModelIndex(), destinationRow: to > from ? to+count : to);
208 qquickmodelviewstestutil_move(from, to, n: count, items: &list);
209 emit endMoveRows();
210}
211
212void QQuickViewTestUtils::QaimModel::modifyItem(int idx, const QString &name, const QString &number)
213{
214 list[idx] = QPair<QString,QString>(name, number);
215 emit dataChanged(topLeft: index(row: idx,column: 0), bottomRight: index(row: idx,column: 0));
216}
217
218void QQuickViewTestUtils::QaimModel::clear()
219{
220 int count = list.size();
221 if (count > 0) {
222 beginRemoveRows(parent: QModelIndex(), first: 0, last: count-1);
223 list.clear();
224 endRemoveRows();
225 }
226}
227
228void QQuickViewTestUtils::QaimModel::reset()
229{
230 emit beginResetModel();
231 emit endResetModel();
232}
233
234void QQuickViewTestUtils::QaimModel::resetItems(const QList<QPair<QString, QString> > &items)
235{
236 beginResetModel();
237 list = items;
238 endResetModel();
239}
240
241class ScopedPrintable
242{
243 Q_DISABLE_COPY_MOVE(ScopedPrintable)
244
245public:
246 ScopedPrintable(const QString &string) : data(QTest::toString(str: string)) {}
247 ~ScopedPrintable() { delete[] data; }
248
249 operator const char*() const { return data; }
250
251private:
252 const char *data;
253};
254
255void QQuickViewTestUtils::QaimModel::matchAgainst(const QList<QPair<QString, QString> > &other, const QString &error1, const QString &error2) {
256 for (int i=0; i<other.size(); i++) {
257 QVERIFY2(list.contains(other[i]),
258 ScopedPrintable(other[i].first + QLatin1Char(' ') + other[i].second + QLatin1Char(' ') + error1));
259 }
260 for (int i=0; i<list.size(); i++) {
261 QVERIFY2(other.contains(list[i]),
262 ScopedPrintable(list[i].first + QLatin1Char(' ') + list[i].second + QLatin1Char(' ') + error2));
263 }
264}
265
266
267
268QQuickViewTestUtils::ListRange::ListRange()
269 : valid(false)
270{
271}
272
273QQuickViewTestUtils::ListRange::ListRange(const ListRange &other)
274 : valid(other.valid)
275{
276 indexes = other.indexes;
277}
278
279QQuickViewTestUtils::ListRange::ListRange(int start, int end)
280 : valid(true)
281{
282 for (int i=start; i<=end; i++)
283 indexes << i;
284}
285
286QQuickViewTestUtils::ListRange::~ListRange()
287{
288}
289
290QQuickViewTestUtils::ListRange QQuickViewTestUtils::ListRange::operator+(const ListRange &other) const
291{
292 if (other == *this)
293 return *this;
294 ListRange a(*this);
295 a.indexes.append(l: other.indexes);
296 return a;
297}
298
299bool QQuickViewTestUtils::ListRange::operator==(const ListRange &other) const
300{
301 return QSet<int>(indexes.cbegin(), indexes.cend())
302 == QSet<int>(other.indexes.cbegin(), other.indexes.cend());
303}
304
305bool QQuickViewTestUtils::ListRange::operator!=(const ListRange &other) const
306{
307 return !(*this == other);
308}
309
310bool QQuickViewTestUtils::ListRange::isValid() const
311{
312 return valid;
313}
314
315int QQuickViewTestUtils::ListRange::count() const
316{
317 return indexes.size();
318}
319
320QList<QPair<QString,QString> > QQuickViewTestUtils::ListRange::getModelDataValues(const QaimModel &model)
321{
322 QList<QPair<QString,QString> > data;
323 if (!valid)
324 return data;
325 for (int i=0; i<indexes.size(); i++)
326 data.append(t: qMakePair(value1: model.name(index: indexes[i]), value2: model.number(index: indexes[i])));
327 return data;
328}
329
330QQuickViewTestUtils::StressTestModel::StressTestModel()
331 : QAbstractListModel()
332 , m_rowCount(20)
333{
334 QTimer *t = new QTimer(this);
335 t->setInterval(500);
336 t->start();
337
338 connect(sender: t, signal: &QTimer::timeout, context: this, slot: &StressTestModel::updateModel);
339}
340
341int QQuickViewTestUtils::StressTestModel::rowCount(const QModelIndex &) const
342{
343 return m_rowCount;
344}
345
346QVariant QQuickViewTestUtils::StressTestModel::data(const QModelIndex &, int) const
347{
348 return QVariant();
349}
350
351void QQuickViewTestUtils::StressTestModel::updateModel()
352{
353 if (m_rowCount > 10) {
354 for (int i = 0; i < 10; ++i) {
355 int rnum = QRandomGenerator::global()->bounded(highest: m_rowCount);
356 beginRemoveRows(parent: QModelIndex(), first: rnum, last: rnum);
357 m_rowCount--;
358 endRemoveRows();
359 }
360 }
361 if (m_rowCount < 20) {
362 for (int i = 0; i < 10; ++i) {
363 int rnum = QRandomGenerator::global()->bounded(highest: m_rowCount);
364 beginInsertRows(parent: QModelIndex(), first: rnum, last: rnum);
365 m_rowCount++;
366 endInsertRows();
367 }
368 }
369}
370
371#if QT_CONFIG(quick_itemview) && defined(QT_BUILD_INTERNAL)
372bool QQuickViewTestUtils::testVisibleItems(const QQuickItemViewPrivate *priv, bool *nonUnique, FxViewItem **failItem, int *expectedIdx)
373{
374 QHash<QQuickItem*, int> uniqueItems;
375
376 int skip = 0;
377 for (int i = 0; i < priv->visibleItems.size(); ++i) {
378 FxViewItem *item = priv->visibleItems.at(i);
379 if (!item) {
380 *failItem = nullptr;
381 return false;
382 }
383#if 0
384 qDebug() << "\t" << item->index
385 << item->item
386 << item->position()
387 << (!item->item || QQuickItemPrivate::get(item->item)->culled ? "hidden" : "visible");
388#endif
389 if (item->index == -1) {
390 ++skip;
391 } else if (item->index != priv->visibleIndex + i - skip) {
392 *nonUnique = false;
393 *failItem = item;
394 *expectedIdx = priv->visibleIndex + i - skip;
395 return false;
396 } else if (uniqueItems.contains(key: item->item)) {
397 *nonUnique = true;
398 *failItem = item;
399 *expectedIdx = uniqueItems.find(key: item->item).value();
400 return false;
401 }
402
403 uniqueItems.insert(key: item->item, value: item->index);
404 }
405
406 return true;
407}
408#endif
409
410namespace QQuickTouchUtils {
411
412 /* QQuickWindow does event compression and only delivers events just
413 * before it is about to render the next frame. Since some tests
414 * rely on events being delivered immediately AND that no other
415 * event processing has occurred in the meanwhile, we flush the
416 * event manually and immediately.
417 */
418 void flush(QQuickWindow *window) {
419 if (!window)
420 return;
421 QQuickDeliveryAgentPrivate *da = QQuickWindowPrivate::get(c: window)->deliveryAgentPrivate();
422 if (!da || !da->delayedTouch)
423 return;
424 da->deliverDelayedTouchEvent();
425 }
426
427}
428
429namespace QTest {
430 int Q_TESTLIB_EXPORT defaultMouseDelay();
431}
432
433namespace QQuickTest {
434
435 /*! \internal
436 Initialize \a view, set \a url, center in available geometry, move mouse away if desired.
437 If \a errorMessage is given, QQuickView::errors() will be concatenated into it;
438 otherwise, the QWARN messages are generally enough to debug the test.
439
440 Returns \c false if the view fails to load the QML. That should be fatal in most tests,
441 so normally the return value should be checked with QVERIFY.
442 */
443 bool initView(QQuickView &view, const QUrl &url, bool moveMouseOut, QByteArray *errorMessage)
444 {
445 const bool platformIsWayland = !QGuiApplication::platformName().compare(other: QLatin1String("wayland"), cs: Qt::CaseInsensitive);
446 view.setSource(url);
447 while (view.status() == QQuickView::Loading)
448 QTest::qWait(ms: 10);
449 if (view.status() != QQuickView::Ready) {
450 if (errorMessage) {
451 for (const QQmlError &e : view.errors())
452 errorMessage->append(a: e.toString().toLocal8Bit() + '\n');
453 }
454 return false;
455 }
456 if (view.width() == 0)
457 view.setWidth(100);
458 if (view.height() == 0)
459 view.setHeight(100);
460 if (!platformIsWayland) {
461 const QSize size = view.size();
462 const QRect screenGeometry = view.screen()->availableGeometry();
463 const QPoint offset = QPoint(size.width() / 2, size.height() / 2);
464 view.setFramePosition(screenGeometry.center() - offset);
465#if QT_CONFIG(cursor) // Get the cursor out of the way. But it's not possible on Wayland.
466 if (moveMouseOut)
467 QCursor::setPos(view.geometry().topRight() + QPoint(100, 100));
468#else
469 Q_UNUSED(moveMouseOut);
470#endif
471 }
472 return true;
473 }
474
475 /*! \internal
476 Initialize \a view, set \a url, center in available geometry, move mouse away,
477 show the \a view, wait for it to be exposed, and verify that its rootObject is not null.
478
479 Returns \c false if anything fails, which should be fatal in most tests.
480 The usual way to call this function is
481 \code
482 QQuickView window;
483 QVERIFY(QQuickTest::showView(window, testFileUrl("myitems.qml")));
484 \endcode
485 */
486 bool showView(QQuickView &view, const QUrl &url)
487 {
488 if (!initView(view, url))
489 return false;
490 view.show();
491 if (!QTest::qWaitForWindowExposed(window: &view))
492 return false;
493 if (!view.rootObject())
494 return false;
495 return true;
496 }
497
498 // TODO maybe move the generic pointerPress/Move/Release functions to QTestLib later on
499
500 static Qt::MouseButton pressedTabletButton = Qt::NoButton;
501 static Qt::KeyboardModifiers pressedTabletModifiers = Qt::NoModifier;
502
503 void pointerPress(const QPointingDevice *dev, QQuickWindow *window, int pointId, const QPoint &p,
504 Qt::MouseButton button, Qt::KeyboardModifiers modifiers, int delay)
505 {
506 const auto defaultDelay = QTest::defaultMouseDelay();
507 switch (dev->type()) {
508 case QPointingDevice::DeviceType::Mouse:
509 case QPointingDevice::DeviceType::TouchPad:
510 QTest::mousePress(window, button, stateKey: modifiers, pos: p, delay: delay >= 0 ? delay : defaultDelay ? defaultDelay : 1);
511 break;
512 case QPointingDevice::DeviceType::TouchScreen:
513 // TODO apply delay when QTBUG-95421 is fixed
514 QTest::touchEvent(window, device: const_cast<QPointingDevice *>(dev)).press(touchId: pointId, pt: p, window);
515 QQuickTouchUtils::flush(window);
516 break;
517 case QPointingDevice::DeviceType::Puck:
518 case QPointingDevice::DeviceType::Stylus:
519 case QPointingDevice::DeviceType::Airbrush:{
520 const QPointF nativeLocal = QHighDpi::toNativeLocalPosition(value: p, context: window);
521 const QPointF nativeGlobal = QHighDpi::toNativeGlobalPosition(value: window->mapToGlobal(pos: p), context: window);
522 QTest::lastMouseTimestamp += delay >= 0 ? delay : defaultDelay ? defaultDelay : 1;
523 pressedTabletButton = button;
524 pressedTabletModifiers = modifiers;
525 QWindowSystemInterface::handleTabletEvent(window, timestamp: QTest::lastMouseTimestamp, device: dev, local: nativeLocal, global: nativeGlobal,
526 buttons: button, pressure: 0.8, xTilt: 0, yTilt: 0, tangentialPressure: 0, rotation: 0, z: 0, modifiers);
527 break;
528 }
529 default:
530 qWarning() << "can't send a press event from" << dev;
531 break;
532 }
533 }
534
535 void pointerMove(const QPointingDevice *dev, QQuickWindow *window, int pointId, const QPoint &p, int delay)
536 {
537 const auto defaultDelay = QTest::defaultMouseDelay();
538 switch (dev->type()) {
539 case QPointingDevice::DeviceType::Mouse:
540 case QPointingDevice::DeviceType::TouchPad:
541 QTest::mouseMove(window, pos: p, delay: delay >= 0 ? delay : defaultDelay ? defaultDelay : 1);
542 break;
543 case QPointingDevice::DeviceType::TouchScreen:
544 // TODO apply delay when QTBUG-95421 is fixed
545 QTest::touchEvent(window, device: const_cast<QPointingDevice *>(dev)).move(touchId: pointId, pt: p, window);
546 QQuickTouchUtils::flush(window);
547 break;
548 case QPointingDevice::DeviceType::Puck:
549 case QPointingDevice::DeviceType::Stylus:
550 case QPointingDevice::DeviceType::Airbrush: {
551 const QPointF nativeLocal = QHighDpi::toNativeLocalPosition(value: p, context: window);
552 const QPointF nativeGlobal = QHighDpi::toNativeGlobalPosition(value: window->mapToGlobal(pos: p), context: window);
553 const auto delay = QTest::defaultMouseDelay();
554 // often QTest::defaultMouseDelay() == 0; but avoid infinite velocity
555 QTest::lastMouseTimestamp += delay >= 0 ? delay : defaultDelay ? defaultDelay : 1;
556 QWindowSystemInterface::handleTabletEvent(window, timestamp: QTest::lastMouseTimestamp, device: dev, local: nativeLocal, global: nativeGlobal,
557 buttons: pressedTabletButton, pressure: pressedTabletButton == Qt::NoButton ? 0 : 0.75,
558 xTilt: 0, yTilt: 0, tangentialPressure: 0, rotation: 0, z: 0, modifiers: pressedTabletModifiers);
559 break;
560 }
561 default:
562 qWarning() << "can't send a move event from" << dev;
563 break;
564 }
565 }
566
567 void pointerRelease(const QPointingDevice *dev, QQuickWindow *window, int pointId, const QPoint &p,
568 Qt::MouseButton button, Qt::KeyboardModifiers modifiers, int delay)
569 {
570 const auto defaultDelay = QTest::defaultMouseDelay();
571 switch (dev->type()) {
572 case QPointingDevice::DeviceType::Mouse:
573 case QPointingDevice::DeviceType::TouchPad:
574 QTest::mouseRelease(window, button, stateKey: modifiers, pos: p, delay: delay >= 0 ? delay : defaultDelay ? defaultDelay : 1);
575 break;
576 case QPointingDevice::DeviceType::TouchScreen:
577 // TODO apply delay when QTBUG-95421 is fixed
578 QTest::touchEvent(window, device: const_cast<QPointingDevice *>(dev)).release(touchId: pointId, pt: p, window);
579 QQuickTouchUtils::flush(window);
580 break;
581 case QPointingDevice::DeviceType::Puck:
582 case QPointingDevice::DeviceType::Stylus:
583 case QPointingDevice::DeviceType::Airbrush: {
584 const QPointF nativeLocal = QHighDpi::toNativeLocalPosition(value: p, context: window);
585 const QPointF nativeGlobal = QHighDpi::toNativeGlobalPosition(value: window->mapToGlobal(pos: p), context: window);
586 QTest::lastMouseTimestamp += delay >= 0 ? delay : defaultDelay ? defaultDelay : 1;
587 QWindowSystemInterface::handleTabletEvent(window, timestamp: QTest::lastMouseTimestamp, device: dev, local: nativeLocal, global: nativeGlobal,
588 buttons: Qt::NoButton, pressure: 0, xTilt: 0, yTilt: 0, tangentialPressure: 0, rotation: 0, z: 0, modifiers);
589 break;
590 }
591 default:
592 qWarning() << "can't send a press event from" << dev;
593 break;
594 }
595 }
596
597 void pointerMoveAndPress(const QPointingDevice *dev, QQuickWindow *window,
598 int pointId, const QPoint &p, Qt::MouseButton button,
599 Qt::KeyboardModifiers modifiers, int delay)
600 {
601 pointerMove(dev, window, pointId, p, delay);
602 pointerPress(dev, window, pointId, p, button, modifiers);
603 }
604
605 void pointerMoveAndRelease(const QPointingDevice *dev, QQuickWindow *window,
606 int pointId, const QPoint &p, Qt::MouseButton button,
607 Qt::KeyboardModifiers modifiers, int delay)
608 {
609 pointerMove(dev, window, pointId, p, delay);
610 pointerRelease(dev, window, pointId, p, button, modifiers);
611 }
612
613 void pointerFlick(const QPointingDevice *dev, QQuickWindow *window,
614 int pointId, const QPoint &from, const QPoint &to, int duration,
615 Qt::MouseButton button, Qt::KeyboardModifiers modifiers, int delay)
616 {
617 const int pointCount = 5;
618 const QPoint diff = to - from;
619
620 // send press, five equally spaced moves, and release.
621 pointerMoveAndPress(dev, window, pointId, p: from, button, modifiers, delay);
622
623 for (int i = 0; i < pointCount; ++i)
624 pointerMove(dev, window, pointId, p: from + (i + 1) * diff / pointCount, delay: duration / pointCount);
625
626 pointerMoveAndRelease(dev, window, pointId, p: to, button, modifiers);
627 }
628}
629
630QT_END_NAMESPACE
631
632#include "moc_viewtestutils_p.cpp"
633

Provided by KDAB

Privacy Policy
Learn Advanced QML with KDAB
Find out more

source code of qtdeclarative/src/quicktestutils/quick/viewtestutils.cpp