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

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