// Copyright (C) 2021 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include "delegatemodelkinds.h" #include #include #include #include #include #include #include #include #include #include #include #include Q_LOGGING_CATEGORY(lcTests, "qt.quick.tests") using namespace QQuickViewTestUtils; using namespace QQuickVisualTestUtils; static const int oneSecondInMs = 1000; class tst_QQuickListView2 : public QQmlDataTest { Q_OBJECT public: tst_QQuickListView2(); private slots: void urlListModel(); void dragDelegateWithMouseArea_data(); void dragDelegateWithMouseArea(); void delegateChooserEnumRole(); void QTBUG_92809(); void footerUpdate(); void singletonModelLifetime(); void delegateModelRefresh(); void wheelSnap(); void wheelSnap_data(); void nestedWheelSnap(); void sectionsNoOverlap(); void metaSequenceAsModel(); void noCrashOnIndexChange(); void innerRequired(); void boundDelegateComponent(); void tapDelegateDuringFlicking_data(); void tapDelegateDuringFlicking(); void flickDuringFlicking_data(); void flickDuringFlicking(); void maxExtent_data(); void maxExtent(); void isCurrentItem_DelegateModel(); void isCurrentItem_NoRegressionWithDelegateModelGroups(); void pullbackSparseList(); void highlightWithBound(); void sectionIsCompatibleWithBoundComponents(); void sectionGeometryChange(); void areaZeroviewDoesNotNeedlesslyPopulateWholeModel(); void viewportAvoidUndesiredMovementOnSetCurrentIndex(); void delegateContextHandling(); void fetchMore_data(); void fetchMore(); void changingOrientationResetsPreviousAxisValues_data(); void changingOrientationResetsPreviousAxisValues(); void bindingDirectlyOnPositionInHeaderAndFooterDelegates_data(); void bindingDirectlyOnPositionInHeaderAndFooterDelegates(); void clearObjectListModel(); void gadgetModelSections(); void visibleBoundToCountGreaterThanZero(); void setDelegateAfterModel(); void delegateModelAccess_data(); void delegateModelAccess(); void removeAndDestroyObjectModelItem_data(); void removeAndDestroyObjectModelItem(); private: void flickWithTouch(QQuickWindow *window, const QPoint &from, const QPoint &to); std::unique_ptr touchscreen{QTest::createTouchDevice()}; }; tst_QQuickListView2::tst_QQuickListView2() : QQmlDataTest(QT_QMLTEST_DATADIR) { } void tst_QQuickListView2::urlListModel() { QScopedPointer window(createView()); QVERIFY(window); QList model = { QUrl::fromLocalFile("abc"), QUrl::fromLocalFile("123") }; window->setInitialProperties({{ "model", QVariant::fromValue(model) }}); window->setSource(testFileUrl("urlListModel.qml")); window->show(); QVERIFY(QTest::qWaitForWindowExposed(window.data())); QQuickListView *view = window->rootObject()->property("view").value(); QVERIFY(view); if (QQuickTest::qIsPolishScheduled(view)) QVERIFY(QQuickTest::qWaitForPolish(view)); QCOMPARE(view->count(), model.size()); } static void dragListView(QWindow *window, QPoint *startPos, const QPoint &delta) { auto drag_helper = [&](QWindow *window, QPoint *startPos, const QPoint &d) { QPoint pos = *startPos; const int dragDistance = d.manhattanLength(); const QPoint unitVector(qBound(-1, d.x(), 1), qBound(-1, d.y(), 1)); for (int i = 0; i < dragDistance; ++i) { QTest::mouseMove(window, pos); pos += unitVector; } // Move to the final position pos = *startPos + d; QTest::mouseMove(window, pos); *startPos = pos; }; if (delta.manhattanLength() == 0) return; const int dragThreshold = QGuiApplication::styleHints()->startDragDistance(); const QPoint unitVector(qBound(-1, delta.x(), 1), qBound(-1, delta.y(), 1)); // go just beyond the drag theshold drag_helper(window, startPos, unitVector * (dragThreshold + 1)); drag_helper(window, startPos, unitVector); // next drag will actually scroll the listview drag_helper(window, startPos, delta); } void tst_QQuickListView2::dragDelegateWithMouseArea_data() { QTest::addColumn("layoutDirection"); for (int layDir = QQuickItemView::LeftToRight; layDir <= (int)QQuickItemView::VerticalBottomToTop; layDir++) { const char *enumValueName = QMetaEnum::fromType().valueToKey(layDir); QTest::newRow(enumValueName) << static_cast(layDir); } } void tst_QQuickListView2::viewportAvoidUndesiredMovementOnSetCurrentIndex() { QScopedPointer window(createView()); QVERIFY(window); window->setFlag(Qt::FramelessWindowHint); window->setSource(testFileUrl("viewportAvoidUndesiredMovementOnSetCurrentIndex.qml")); window->show(); QVERIFY(QTest::qWaitForWindowExposed(window.data())); QVERIFY(window->rootObject()); QQuickListView *listview = findItem(window->rootObject(), "list"); QVERIFY(listview); listview->setCurrentIndex(2); // change current item // partially obscure first item QCOMPARE(listview->contentY(), 0); listview->setContentY(50); QTRY_COMPARE(listview->contentY(), 50); listview->setCurrentIndex(0); // change current item back to first one QVERIFY(QQuickTest::qWaitForPolish(listview)); // that shouldn't have caused any movement QCOMPARE(listview->contentY(), 50); // that even applies to the case where the current item is completely out of the viewport listview->setCurrentIndex(25); QVERIFY(QQuickTest::qWaitForPolish(listview)); QCOMPARE(listview->contentY(), 50); } void tst_QQuickListView2::dragDelegateWithMouseArea() { QFETCH(QQuickItemView::LayoutDirection, layoutDirection); QScopedPointer window(createView()); QVERIFY(window); window->setFlag(Qt::FramelessWindowHint); window->setSource(testFileUrl("delegateWithMouseArea.qml")); window->show(); QVERIFY(QTest::qWaitForWindowExposed(window.data())); QQuickListView *listview = findItem(window->rootObject(), "list"); QVERIFY(listview != nullptr); const bool horizontal = layoutDirection < QQuickItemView::VerticalTopToBottom; listview->setOrientation(horizontal ? QQuickListView::Horizontal : QQuickListView::Vertical); if (horizontal) listview->setLayoutDirection(static_cast(layoutDirection)); else listview->setVerticalLayoutDirection(static_cast(layoutDirection)); QVERIFY(QQuickTest::qWaitForPolish(listview)); auto contentPosition = [&](QQuickListView *listview) { return (listview->orientation() == QQuickListView::Horizontal ? listview->contentX(): listview->contentY()); }; qreal expectedContentPosition = contentPosition(listview); QPoint startPos = (QPointF(listview->width(), listview->height())/2).toPoint(); QTest::mousePress(window.data(), Qt::LeftButton, Qt::NoModifier, startPos, 200); QPoint dragDelta(0, -10); if (layoutDirection == QQuickItemView::RightToLeft || layoutDirection == QQuickItemView::VerticalBottomToTop) dragDelta = -dragDelta; expectedContentPosition -= dragDelta.y(); if (horizontal) dragDelta = dragDelta.transposed(); dragListView(window.data(), &startPos, dragDelta); QTest::mouseRelease(window.data(), Qt::LeftButton, Qt::NoModifier, startPos, 200); // Wait 200 ms before we release to avoid trigger a flick // wait for the "fixup" animation to finish QVERIFY(QTest::qWaitFor([&]() { return !listview->isMoving();} )); QCOMPARE(contentPosition(listview), expectedContentPosition); } void tst_QQuickListView2::delegateChooserEnumRole() { QQuickView window; QVERIFY(QQuickTest::showView(window, testFileUrl("delegateChooserEnumRole.qml"))); QQuickListView *listview = qobject_cast(window.rootObject()); QVERIFY(listview); QTRY_COMPARE(listview->count(), 3); QCOMPARE(listview->itemAtIndex(0)->property("delegateType").toInt(), 0); QCOMPARE(listview->itemAtIndex(1)->property("delegateType").toInt(), 1); QCOMPARE(listview->itemAtIndex(2)->property("delegateType").toInt(), 2); } void tst_QQuickListView2::QTBUG_92809() { QScopedPointer window(createView()); QTRY_VERIFY(window); window->setSource(testFileUrl("qtbug_92809.qml")); window->show(); QVERIFY(QTest::qWaitForWindowExposed(window.data())); QQuickListView *listview = findItem(window->rootObject(), "list"); QTRY_VERIFY(listview != nullptr); QVERIFY(QQuickTest::qWaitForPolish(listview)); listview->setCurrentIndex(1); QVERIFY(QQuickTest::qWaitForPolish(listview)); listview->setCurrentIndex(2); QVERIFY(QQuickTest::qWaitForPolish(listview)); listview->setCurrentIndex(3); QVERIFY(QQuickTest::qWaitForPolish(listview)); QTest::qWait(500); listview->setCurrentIndex(10); QVERIFY(QQuickTest::qWaitForPolish(listview)); QTest::qWait(500); int currentIndex = listview->currentIndex(); QTRY_COMPARE(currentIndex, 9); } void tst_QQuickListView2::footerUpdate() { QScopedPointer window(createView()); QTRY_VERIFY(window); window->setSource(testFileUrl("footerUpdate.qml")); window->show(); QVERIFY(QTest::qWaitForWindowExposed(window.data())); QQuickListView *listview = findItem(window->rootObject(), "list"); QTRY_VERIFY(listview != nullptr); QVERIFY(QQuickTest::qWaitForPolish(listview)); QQuickItem *footer = listview->footerItem(); QTRY_VERIFY(footer); QVERIFY(QQuickTest::qWaitForPolish(footer)); QTRY_COMPARE(footer->y(), 0); } void tst_QQuickListView2::sectionsNoOverlap() { QScopedPointer window(createView()); QTRY_VERIFY(window); window->setSource(testFileUrl("sectionsNoOverlap.qml")); window->show(); QVERIFY(QTest::qWaitForWindowExposed(window.data())); QQuickListView *listview = findItem(window->rootObject(), "list"); QTRY_VERIFY(listview != nullptr); QQuickItem *contentItem = listview->contentItem(); QTRY_VERIFY(contentItem != nullptr); QVERIFY(QQuickTest::qWaitForPolish(listview)); const unsigned int sectionCount = 2, normalDelegateCount = 2; const unsigned int expectedSectionHeight = 48; const unsigned int expectedNormalDelegateHeight = 40; unsigned int normalDelegateCounter = 0; for (unsigned int sectionIndex = 0; sectionIndex < sectionCount; ++sectionIndex) { QQuickItem *sectionDelegate = findItem(contentItem, "section" + QString::number(sectionIndex + 1)); QVERIFY(sectionDelegate); QCOMPARE(sectionDelegate->height(), expectedSectionHeight); QVERIFY(sectionDelegate->isVisible()); QCOMPARE(sectionDelegate->y(), qreal(sectionIndex * expectedSectionHeight + (sectionIndex * normalDelegateCount * expectedNormalDelegateHeight))); for (; normalDelegateCounter < ((sectionIndex + 1) * normalDelegateCount); ++normalDelegateCounter) { QQuickItem *normalDelegate = findItem( contentItem, "element" + QString::number(normalDelegateCounter + 1)); QVERIFY(normalDelegate); QCOMPARE(normalDelegate->height(), expectedNormalDelegateHeight); QVERIFY(normalDelegate->isVisible()); QCOMPARE(normalDelegate->y(), qreal((sectionIndex + 1) * expectedSectionHeight + normalDelegateCounter * expectedNormalDelegateHeight + listview->spacing() * normalDelegateCounter)); } } } void tst_QQuickListView2::metaSequenceAsModel() { QQmlEngine engine; QQmlComponent c(&engine, testFileUrl("metaSequenceAsModel.qml")); QVERIFY2(c.isReady(), qPrintable(c.errorString())); QScopedPointer o(c.create()); QVERIFY(!o.isNull()); QStringList strings = qvariant_cast(o->property("texts")); QCOMPARE(strings.size(), 2); QCOMPARE(strings[0], QStringLiteral("1/2")); QCOMPARE(strings[1], QStringLiteral("5/6")); } void tst_QQuickListView2::noCrashOnIndexChange() { QQmlEngine engine; QQmlComponent c(&engine, testFileUrl("noCrashOnIndexChange.qml")); QVERIFY2(c.isReady(), qPrintable(c.errorString())); QScopedPointer o(c.create()); QVERIFY(!o.isNull()); QObject *delegateModel = qmlContext(o.data())->objectForName("displayDelegateModel"); QVERIFY(delegateModel); QObject *items = qvariant_cast(delegateModel->property("items")); QCOMPARE(items->property("name").toString(), QStringLiteral("items")); QCOMPARE(items->property("count").toInt(), 4); } void tst_QQuickListView2::innerRequired() { QQmlEngine engine; const QUrl url(testFileUrl("innerRequired.qml")); QQmlComponent component(&engine, url); QVERIFY2(component.isReady(), qPrintable(component.errorString())); QScopedPointer o(component.create()); QVERIFY2(!o.isNull(), qPrintable(component.errorString())); QQuickListView *a = qobject_cast( qmlContext(o.data())->objectForName(QStringLiteral("listView"))); QVERIFY(a); QCOMPARE(a->count(), 2); QCOMPARE(a->itemAtIndex(0)->property("age").toInt(), 8); QCOMPARE(a->itemAtIndex(0)->property("text").toString(), u"meow"); QCOMPARE(a->itemAtIndex(1)->property("age").toInt(), 5); QCOMPARE(a->itemAtIndex(1)->property("text").toString(), u"woof"); } void tst_QQuickListView2::boundDelegateComponent() { QQmlEngine engine; const QUrl url(testFileUrl("boundDelegateComponent.qml")); QQmlComponent c(&engine, url); QVERIFY2(c.isReady(), qPrintable(c.errorString())); QTest::ignoreMessage( QtWarningMsg, qPrintable(QLatin1String("%1:14: ReferenceError: index is not defined") .arg(url.toString()))); QScopedPointer o(c.create()); QVERIFY(!o.isNull()); QQmlContext *context = qmlContext(o.data()); QObject *inner = context->objectForName(QLatin1String("listView")); QVERIFY(inner != nullptr); QQuickListView *listView = qobject_cast(inner); QVERIFY(listView != nullptr); QObject *item = listView->itemAtIndex(0); QVERIFY(item); QCOMPARE(item->objectName(), QLatin1String("fooouterundefined")); QObject *inner2 = context->objectForName(QLatin1String("listView2")); QVERIFY(inner2 != nullptr); QQuickListView *listView2 = qobject_cast(inner2); QVERIFY(listView2 != nullptr); QObject *item2 = listView2->itemAtIndex(0); QVERIFY(item2); QCOMPARE(item2->objectName(), QLatin1String("fooouter0")); QQmlComponent *comp = qobject_cast( context->objectForName(QLatin1String("outerComponent"))); QVERIFY(comp != nullptr); for (int i = 0; i < 3; ++i) { QTest::ignoreMessage( QtWarningMsg, qPrintable(QLatin1String("%1:51:21: ReferenceError: model is not defined") .arg(url.toString()))); } QScopedPointer outerItem(comp->create(context)); QVERIFY(!outerItem.isNull()); QQuickListView *innerListView = qobject_cast( qmlContext(outerItem.data())->objectForName(QLatin1String("innerListView"))); QVERIFY(innerListView != nullptr); QCOMPARE(innerListView->count(), 3); for (int i = 0; i < 3; ++i) QVERIFY(innerListView->itemAtIndex(i)->objectName().isEmpty()); } void tst_QQuickListView2::tapDelegateDuringFlicking_data() { QTest::addColumn("qmlFile"); QTest::addColumn("boundsBehavior"); QTest::addColumn("expectCanceled"); QTest::newRow("Button StopAtBounds") << QByteArray("buttonDelegate.qml") << QQuickFlickable::BoundsBehavior(QQuickFlickable::StopAtBounds) << false; QTest::newRow("MouseArea StopAtBounds") << QByteArray("mouseAreaDelegate.qml") << QQuickFlickable::BoundsBehavior(QQuickFlickable::StopAtBounds) << true; QTest::newRow("Button DragOverBounds") << QByteArray("buttonDelegate.qml") << QQuickFlickable::BoundsBehavior(QQuickFlickable::DragOverBounds) << false; QTest::newRow("MouseArea DragOverBounds") << QByteArray("mouseAreaDelegate.qml") << QQuickFlickable::BoundsBehavior(QQuickFlickable::DragOverBounds) << true; QTest::newRow("Button OvershootBounds") << QByteArray("buttonDelegate.qml") << QQuickFlickable::BoundsBehavior(QQuickFlickable::OvershootBounds) << false; QTest::newRow("MouseArea OvershootBounds") << QByteArray("mouseAreaDelegate.qml") << QQuickFlickable::BoundsBehavior(QQuickFlickable::OvershootBounds) << true; QTest::newRow("Button DragAndOvershootBounds") << QByteArray("buttonDelegate.qml") << QQuickFlickable::BoundsBehavior(QQuickFlickable::DragAndOvershootBounds) << false; QTest::newRow("MouseArea DragAndOvershootBounds") << QByteArray("mouseAreaDelegate.qml") << QQuickFlickable::BoundsBehavior(QQuickFlickable::DragAndOvershootBounds) << true; } void tst_QQuickListView2::tapDelegateDuringFlicking() // QTBUG-103832 { QFETCH(QByteArray, qmlFile); QFETCH(QQuickFlickable::BoundsBehavior, boundsBehavior); QFETCH(bool, expectCanceled); QQuickView window; QVERIFY(QQuickTest::showView(window, testFileUrl(qmlFile.constData()))); QQuickListView *listView = qobject_cast(window.rootObject()); QVERIFY(listView); listView->setBoundsBehavior(boundsBehavior); flickWithTouch(&window, {100, 400}, {100, 100}); QTRY_VERIFY(listView->contentY() > 501); // let it flick some distance QVERIFY(listView->isFlicking()); // we want to test the case when it's still moving while we tap QVERIFY(listView->isMoving()); // @y = 400 we pressed the 4th delegate; started flicking, and the press was canceled QCOMPARE(listView->property("pressedDelegates").toList().first(), 4); // At first glance one would expect MouseArea and Button would be consistent about this; // but in fact, before ListView takes over the grab via filtering, // Button.pressed transitions to false because QQuickAbstractButtonPrivate::handleMove // sees that the touchpoint has strayed outside its bounds, but it does NOT emit the canceled signal if (expectCanceled) { const QVariantList canceledDelegates = listView->property("canceledDelegates").toList(); QCOMPARE(canceledDelegates.size(), 1); QCOMPARE(canceledDelegates.first(), 4); } QCOMPARE(listView->property("releasedDelegates").toList().size(), 0); // press a delegate during flicking (at y > 501 + 100, so likely delegate 6) QTest::touchEvent(&window, touchscreen.get()).press(0, {100, 100}); QQuickTouchUtils::flush(&window); // The press will stop listView from flicking, but it will still be "moving", which // means that the user is still interacting with it QVERIFY(!listView->isFlicking()); QVERIFY(!listView->isDragging()); QVERIFY(listView->isMoving()); QTest::touchEvent(&window, touchscreen.get()).release(0, {100, 100}); QQuickTouchUtils::flush(&window); // Releasing while "flicking" is false will stop the flicking // session, and set "moving" to false QVERIFY(!listView->isMoving()); QVERIFY(!listView->isFlicking()); QVERIFY(!listView->isDragging()); const QVariantList pressedDelegates = listView->property("pressedDelegates").toList(); const QVariantList releasedDelegates = listView->property("releasedDelegates").toList(); const QVariantList tappedDelegates = listView->property("tappedDelegates").toList(); const QVariantList canceledDelegates = listView->property("canceledDelegates").toList(); qCDebug(lcTests) << "pressed" << pressedDelegates; // usually [4, 6] qCDebug(lcTests) << "released" << releasedDelegates; qCDebug(lcTests) << "tapped" << tappedDelegates; qCDebug(lcTests) << "canceled" << canceledDelegates; // Since the flickable was already moving (that is, the user was interacting with // it), the second tap was only used to stop the flicking. Hence, no delegates // received any pointer events. const int lastPressed = pressedDelegates.last().toInt(); QCOMPARE(lastPressed, 4); // Nothing changed since the beginning QVERIFY(releasedDelegates.isEmpty()); QVERIFY(tappedDelegates.isEmpty()); QCOMPARE(canceledDelegates.size(), expectCanceled ? 1 : 0); // only the first press was canceled, not the second } void tst_QQuickListView2::flickDuringFlicking_data() { QTest::addColumn("qmlFile"); QTest::addColumn("boundsBehavior"); QTest::newRow("Button StopAtBounds") << QByteArray("buttonDelegate.qml") << QQuickFlickable::BoundsBehavior(QQuickFlickable::StopAtBounds); QTest::newRow("MouseArea StopAtBounds") << QByteArray("mouseAreaDelegate.qml") << QQuickFlickable::BoundsBehavior(QQuickFlickable::StopAtBounds); QTest::newRow("Button DragOverBounds") << QByteArray("buttonDelegate.qml") << QQuickFlickable::BoundsBehavior(QQuickFlickable::DragOverBounds); QTest::newRow("MouseArea DragOverBounds") << QByteArray("mouseAreaDelegate.qml") << QQuickFlickable::BoundsBehavior(QQuickFlickable::DragOverBounds); QTest::newRow("Button OvershootBounds") << QByteArray("buttonDelegate.qml") << QQuickFlickable::BoundsBehavior(QQuickFlickable::OvershootBounds); QTest::newRow("MouseArea OvershootBounds") << QByteArray("mouseAreaDelegate.qml") << QQuickFlickable::BoundsBehavior(QQuickFlickable::OvershootBounds); QTest::newRow("Button DragAndOvershootBounds") << QByteArray("buttonDelegate.qml") << QQuickFlickable::BoundsBehavior(QQuickFlickable::DragAndOvershootBounds); QTest::newRow("MouseArea DragAndOvershootBounds") << QByteArray("mouseAreaDelegate.qml") << QQuickFlickable::BoundsBehavior(QQuickFlickable::DragAndOvershootBounds); } void tst_QQuickListView2::flickDuringFlicking() // QTBUG-103832 { QFETCH(QByteArray, qmlFile); QFETCH(QQuickFlickable::BoundsBehavior, boundsBehavior); QQuickView window; QVERIFY(QQuickTest::showView(window, testFileUrl(qmlFile.constData()))); QQuickListView *listView = qobject_cast(window.rootObject()); QVERIFY(listView); listView->setBoundsBehavior(boundsBehavior); flickWithTouch(&window, {100, 400}, {100, 100}); // let it flick some distance QTRY_COMPARE_GT(listView->contentY(), 500); QVERIFY(listView->isFlicking()); // we want to test the case when it's moving and then we flick again const qreal posBeforeSecondFlick = listView->contentY(); // flick again during flicking, and make sure that it doesn't jump back to the first delegate, // but flicks incrementally further from the position at that time QTest::touchEvent(&window, touchscreen.get()).press(0, {100, 400}); QQuickTouchUtils::flush(&window); qCDebug(lcTests) << "second press: contentY" << posBeforeSecondFlick << "->" << listView->contentY(); qCDebug(lcTests) << "pressed delegates" << listView->property("pressedDelegates").toList(); QVERIFY(listView->contentY() >= posBeforeSecondFlick); QTest::qWait(20); QTest::touchEvent(&window, touchscreen.get()).move(0, {100, 300}); QQuickTouchUtils::flush(&window); qCDebug(lcTests) << "first move after second press: contentY" << posBeforeSecondFlick << "->" << listView->contentY(); QVERIFY(listView->contentY() >= posBeforeSecondFlick); QTest::qWait(20); QTest::touchEvent(&window, touchscreen.get()).move(0, {100, 200}); QQuickTouchUtils::flush(&window); qCDebug(lcTests) << "second move after second press: contentY" << posBeforeSecondFlick << "->" << listView->contentY(); QVERIFY(listView->contentY() >= posBeforeSecondFlick + 100); QTest::touchEvent(&window, touchscreen.get()).release(0, {100, 100}); } void tst_QQuickListView2::flickWithTouch(QQuickWindow *window, const QPoint &from, const QPoint &to) { QTest::touchEvent(window, touchscreen.get()).press(0, from, window); QQuickTouchUtils::flush(window); QPoint diff = to - from; for (int i = 1; i <= 8; ++i) { QTest::touchEvent(window, touchscreen.get()).move(0, from + i * diff / 8, window); QQuickTouchUtils::flush(window); } QTest::touchEvent(window, touchscreen.get()).release(0, to, window); QQuickTouchUtils::flush(window); } class SingletonModel : public QStringListModel { Q_OBJECT public: SingletonModel(QObject* parent = nullptr) : QStringListModel(parent) { } }; void tst_QQuickListView2::singletonModelLifetime() { // this does not really test any functionality of listview, but we do not have a good way // to unit test QQmlAdaptorModel in isolation. qmlRegisterSingletonType("SingletonModelLifeTimeTest", 1, 0, "SingletonModel", [](QQmlEngine* , QJSEngine*) -> QObject* { return new SingletonModel; }); QQmlApplicationEngine engine(testFile("singletonModelLifetime.qml")); // needs event loop iteration for callLater to execute QTRY_VERIFY(engine.rootObjects().first()->property("alive").toBool()); } void tst_QQuickListView2::delegateModelRefresh() { // Test case originates from QTBUG-100161 QQmlApplicationEngine engine(testFile("delegateModelRefresh.qml")); QVERIFY(!engine.rootObjects().isEmpty()); // needs event loop iteration for callLater to execute QTRY_VERIFY(engine.rootObjects().first()->property("done").toBool()); } void tst_QQuickListView2::wheelSnap() { QFETCH(QQuickListView::Orientation, orientation); QFETCH(Qt::LayoutDirection, layoutDirection); QFETCH(QQuickItemView::VerticalLayoutDirection, verticalLayoutDirection); QFETCH(QQuickItemView::HighlightRangeMode, highlightRangeMode); QFETCH(QPoint, forwardAngleDelta); QFETCH(qreal, snapAlignment); QFETCH(qreal, endExtent); QFETCH(qreal, startExtent); QFETCH(qreal, preferredHighlightBegin); QFETCH(qreal, preferredHighlightEnd); // Helpers begin quint64 timestamp = 10; auto sendWheelEvent = [×tamp](QQuickView *window, const QPoint &angleDelta) { QPoint pos(100, 100); QWheelEvent event(pos, window->mapToGlobal(pos), QPoint(), angleDelta, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, false); event.setAccepted(false); event.setTimestamp(timestamp); QGuiApplication::sendEvent(window, &event); timestamp += 50; }; auto atEnd = [&layoutDirection, &orientation, &verticalLayoutDirection](QQuickListView *listview) { if (orientation == QQuickListView::Horizontal) { if (layoutDirection == Qt::LeftToRight) return listview->isAtXEnd(); return listview->isAtXBeginning(); } else { if (verticalLayoutDirection == QQuickItemView::VerticalLayoutDirection::TopToBottom) return listview->isAtYEnd(); return listview->isAtYBeginning(); } }; auto atBegin = [&layoutDirection, &orientation, &verticalLayoutDirection](QQuickListView *listview) { if (orientation == QQuickListView::Horizontal) { if (layoutDirection == Qt::LeftToRight) return listview->isAtXBeginning(); return listview->isAtXEnd(); } else { if (verticalLayoutDirection == QQuickItemView::VerticalLayoutDirection::TopToBottom) return listview->isAtYBeginning(); return listview->isAtYEnd(); } }; // Helpers end QScopedPointer window(createView()); QTRY_VERIFY(window); QQuickViewTestUtils::moveMouseAway(window.data()); window->setSource(testFileUrl("snapOneItem.qml")); window->show(); QVERIFY(QTest::qWaitForWindowExposed(window.data())); QQuickListView *listview = qobject_cast(window->rootObject()); QTRY_VERIFY(listview); listview->setOrientation(orientation); listview->setVerticalLayoutDirection(verticalLayoutDirection); listview->setLayoutDirection(layoutDirection); listview->setHighlightRangeMode(highlightRangeMode); listview->setPreferredHighlightBegin(preferredHighlightBegin); listview->setPreferredHighlightEnd(preferredHighlightEnd); QVERIFY(QQuickTest::qWaitForPolish(listview)); QQuickItem *contentItem = listview->contentItem(); QTRY_VERIFY(contentItem); QSignalSpy currentIndexSpy(listview, &QQuickListView::currentIndexChanged); // confirm that a flick hits the next item boundary int indexCounter = 0; sendWheelEvent(window.data(), forwardAngleDelta); QTRY_VERIFY(listview->isMoving() == false); // wait until it stops if (orientation == QQuickListView::Vertical) QCOMPARE(listview->contentY(), snapAlignment); else QCOMPARE(listview->contentX(), snapAlignment); if (highlightRangeMode == QQuickItemView::StrictlyEnforceRange) { ++indexCounter; QTRY_VERIFY(listview->currentIndex() == indexCounter); } // flick to end do { sendWheelEvent(window.data(), forwardAngleDelta); QTRY_VERIFY(listview->isMoving() == false); // wait until it stops if (highlightRangeMode == QQuickItemView::StrictlyEnforceRange) { ++indexCounter; QTRY_VERIFY(listview->currentIndex() == indexCounter); } } while (!atEnd(listview)); if (orientation == QQuickListView::Vertical) QCOMPARE(listview->contentY(), endExtent); else QCOMPARE(listview->contentX(), endExtent); if (highlightRangeMode == QQuickItemView::StrictlyEnforceRange) { QCOMPARE(listview->currentIndex(), listview->count() - 1); QCOMPARE(currentIndexSpy.count(), listview->count() - 1); } // flick to start const QPoint backwardAngleDelta(-forwardAngleDelta.x(), -forwardAngleDelta.y()); do { sendWheelEvent(window.data(), backwardAngleDelta); QTRY_VERIFY(listview->isMoving() == false); // wait until it stops if (highlightRangeMode == QQuickItemView::StrictlyEnforceRange) { --indexCounter; QTRY_VERIFY(listview->currentIndex() == indexCounter); } } while (!atBegin(listview)); if (orientation == QQuickListView::Vertical) QCOMPARE(listview->contentY(), startExtent); else QCOMPARE(listview->contentX(), startExtent); if (highlightRangeMode == QQuickItemView::StrictlyEnforceRange) { QCOMPARE(listview->currentIndex(), 0); QCOMPARE(currentIndexSpy.count(), (listview->count() - 1) * 2); } } void tst_QQuickListView2::wheelSnap_data() { QTest::addColumn("orientation"); QTest::addColumn("layoutDirection"); QTest::addColumn("verticalLayoutDirection"); QTest::addColumn("highlightRangeMode"); QTest::addColumn("forwardAngleDelta"); QTest::addColumn("snapAlignment"); QTest::addColumn("endExtent"); QTest::addColumn("startExtent"); QTest::addColumn("preferredHighlightBegin"); QTest::addColumn("preferredHighlightEnd"); QTest::newRow("vertical, top to bottom") << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::TopToBottom << QQuickItemView::NoHighlightRange << QPoint(20, -240) << 200.0 << 600.0 << 0.0 << 0.0 << 0.0; QTest::newRow("vertical, bottom to top") << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::BottomToTop << QQuickItemView::NoHighlightRange << QPoint(20, 240) << -400.0 << -800.0 << -200.0 << 0.0 << 0.0; QTest::newRow("horizontal, left to right") << QQuickListView::Horizontal << Qt::LeftToRight << QQuickItemView::TopToBottom << QQuickItemView::NoHighlightRange << QPoint(-240, 20) << 200.0 << 600.0 << 0.0 << 0.0 << 0.0; QTest::newRow("horizontal, right to left") << QQuickListView::Horizontal << Qt::RightToLeft << QQuickItemView::TopToBottom << QQuickItemView::NoHighlightRange << QPoint(240, 20) << -400.0 << -800.0 << -200.0 << 0.0 << 0.0; QTest::newRow("vertical, top to bottom, enforce range") << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::TopToBottom << QQuickItemView::StrictlyEnforceRange << QPoint(20, -240) << 200.0 << 600.0 << 0.0 << 0.0 << 0.0; QTest::newRow("vertical, bottom to top, enforce range") << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::BottomToTop << QQuickItemView::StrictlyEnforceRange << QPoint(20, 240) << -400.0 << -800.0 << -200.0 << 0.0 << 0.0; QTest::newRow("horizontal, left to right, enforce range") << QQuickListView::Horizontal << Qt::LeftToRight << QQuickItemView::TopToBottom << QQuickItemView::StrictlyEnforceRange << QPoint(-240, 20) << 200.0 << 600.0 << 0.0 << 0.0 << 0.0; QTest::newRow("horizontal, right to left, enforce range") << QQuickListView::Horizontal << Qt::RightToLeft << QQuickItemView::TopToBottom << QQuickItemView::StrictlyEnforceRange << QPoint(240, 20) << -400.0 << -800.0 << -200.0 << 0.0 << 0.0; QTest::newRow("vertical, top to bottom, apply range") << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::TopToBottom << QQuickItemView::ApplyRange << QPoint(20, -240) << 200.0 << 600.0 << 0.0 << 0.0 << 0.0; QTest::newRow("vertical, bottom to top, apply range") << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::BottomToTop << QQuickItemView::ApplyRange << QPoint(20, 240) << -400.0 << -800.0 << -200.0 << 0.0 << 0.0; QTest::newRow("horizontal, left to right, apply range") << QQuickListView::Horizontal << Qt::LeftToRight << QQuickItemView::TopToBottom << QQuickItemView::ApplyRange << QPoint(-240, 20) << 200.0 << 600.0 << 0.0 << 0.0 << 0.0; QTest::newRow("horizontal, right to left, apply range") << QQuickListView::Horizontal << Qt::RightToLeft << QQuickItemView::TopToBottom << QQuickItemView::ApplyRange << QPoint(240, 20) << -400.0 << -800.0 << -200.0 << 0.0 << 0.0; QTest::newRow("vertical, top to bottom with highlightRange") << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::TopToBottom << QQuickItemView::NoHighlightRange << QPoint(20, -240) << 190.0 << 600.0 << 0.0 << 10.0 << 210.0; QTest::newRow("vertical, bottom to top with highlightRange") << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::BottomToTop << QQuickItemView::NoHighlightRange << QPoint(20, 240) << -390.0 << -800.0 << -200.0 << 10.0 << 210.0; QTest::newRow("horizontal, left to right with highlightRange") << QQuickListView::Horizontal << Qt::LeftToRight << QQuickItemView::TopToBottom << QQuickItemView::NoHighlightRange << QPoint(-240, 20) << 190.0 << 600.0 << 0.0 << 10.0 << 210.0; QTest::newRow("horizontal, right to left with highlightRange") << QQuickListView::Horizontal << Qt::RightToLeft << QQuickItemView::TopToBottom << QQuickItemView::NoHighlightRange << QPoint(240, 20) << -390.0 << -800.0 << -200.0 << 10.0 << 210.0; QTest::newRow("vertical, top to bottom, enforce range with highlightRange") << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::TopToBottom << QQuickItemView::StrictlyEnforceRange << QPoint(20, -240) << 190.0 << 590.0 << -10.0 << 10.0 << 210.0; QTest::newRow("vertical, bottom to top, enforce range with highlightRange") << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::BottomToTop << QQuickItemView::StrictlyEnforceRange << QPoint(20, 240) << -390.0 << -790.0 << -190.0 << 10.0 << 210.0; QTest::newRow("horizontal, left to right, enforce range with highlightRange") << QQuickListView::Horizontal << Qt::LeftToRight << QQuickItemView::TopToBottom << QQuickItemView::StrictlyEnforceRange << QPoint(-240, 20) << 190.0 << 590.0 << -10.0 << 10.0 << 210.0; QTest::newRow("horizontal, right to left, enforce range with highlightRange") << QQuickListView::Horizontal << Qt::RightToLeft << QQuickItemView::TopToBottom << QQuickItemView::StrictlyEnforceRange << QPoint(240, 20) << -390.0 << -790.0 << -190.0 << 10.0 << 210.0; QTest::newRow("vertical, top to bottom, apply range with highlightRange") << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::TopToBottom << QQuickItemView::ApplyRange << QPoint(20, -240) << 190.0 << 600.0 << 0.0 << 10.0 << 210.0; QTest::newRow("vertical, bottom to top, apply range with highlightRange") << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::BottomToTop << QQuickItemView::ApplyRange << QPoint(20, 240) << -390.0 << -800.0 << -200.0 << 10.0 << 210.0; QTest::newRow("horizontal, left to right, apply range with highlightRange") << QQuickListView::Horizontal << Qt::LeftToRight << QQuickItemView::TopToBottom << QQuickItemView::ApplyRange << QPoint(-240, 20) << 190.0 << 600.0 << 0.0 << 10.0 << 210.0; QTest::newRow("horizontal, right to left, apply range with highlightRange") << QQuickListView::Horizontal << Qt::RightToLeft << QQuickItemView::TopToBottom << QQuickItemView::ApplyRange << QPoint(240, 20) << -390.0 << -800.0 << -200.0 << 10.0 << 210.0; } void tst_QQuickListView2::nestedWheelSnap() { QQuickView window; QVERIFY(QQuickTest::showView(window, testFileUrl("nestedSnap.qml"))); quint64 timestamp = 10; auto sendWheelEvent = [×tamp, &window](const QPoint &pixelDelta, Qt::ScrollPhase phase) { const QPoint pos(100, 100); QWheelEvent event(pos, window.mapToGlobal(pos), pixelDelta, pixelDelta, Qt::NoButton, Qt::NoModifier, phase, false, Qt::MouseEventSynthesizedBySystem); event.setAccepted(false); event.setTimestamp(timestamp); QGuiApplication::sendEvent(&window, &event); timestamp += 50; }; QQuickListView *outerListView = qobject_cast(window.rootObject()); QTRY_VERIFY(outerListView); QSignalSpy outerCurrentIndexSpy(outerListView, &QQuickListView::currentIndexChanged); int movingAtIndex = -1; // send horizontal pixel-delta wheel events with phases; confirm that ListView hits the next item boundary sendWheelEvent({}, Qt::ScrollBegin); for (int i = 1; i < 4; ++i) { sendWheelEvent({-50, 0}, Qt::ScrollUpdate); if (movingAtIndex < 0 && outerListView->isMoving()) movingAtIndex = i; } QVERIFY(outerListView->isDragging()); sendWheelEvent({}, Qt::ScrollEnd); QCOMPARE(outerListView->isDragging(), false); QTRY_COMPARE(outerListView->isMoving(), false); // wait until it stops qCDebug(lcTests) << "outer got moving after" << movingAtIndex << "horizontal events; stopped at" << outerListView->contentX() << outerListView->currentIndex(); QCOMPARE_GT(movingAtIndex, 0); QCOMPARE(outerListView->contentX(), 300); QCOMPARE(outerCurrentIndexSpy.size(), 1); movingAtIndex = -1; QQuickListView *innerListView = qobject_cast(outerListView->currentItem()); QTRY_VERIFY(innerListView); QSignalSpy innerCurrentIndexSpy(innerListView, &QQuickListView::currentIndexChanged); // send vertical pixel-delta wheel events with phases; confirm that ListView hits the next item boundary sendWheelEvent({}, Qt::ScrollBegin); for (int i = 1; i < 4; ++i) { sendWheelEvent({0, -50}, Qt::ScrollUpdate); if (movingAtIndex < 0 && innerListView->isMoving()) movingAtIndex = i; } QVERIFY(innerListView->isDragging()); sendWheelEvent({}, Qt::ScrollEnd); QCOMPARE(innerListView->isDragging(), false); QTRY_COMPARE(innerListView->isMoving(), false); // wait until it stops qCDebug(lcTests) << "inner got moving after" << movingAtIndex << "vertical events; stopped at" << innerListView->contentY() << innerListView->currentIndex(); QCOMPARE_GT(movingAtIndex, 0); QCOMPARE(innerListView->contentY(), 300); QCOMPARE(innerCurrentIndexSpy.size(), 1); } class FriendlyItemView : public QQuickItemView { friend class ItemViewAccessor; }; class ItemViewAccessor { public: ItemViewAccessor(QQuickItemView *itemView) : mItemView(reinterpret_cast(itemView)) { } qreal maxXExtent() const { return mItemView->maxXExtent(); } qreal maxYExtent() const { return mItemView->maxYExtent(); } private: FriendlyItemView *mItemView = nullptr; }; void tst_QQuickListView2::maxExtent_data() { QTest::addColumn("qmlFilePath"); QTest::addRow("maxXExtent") << "maxXExtent.qml"; QTest::addRow("maxYExtent") << "maxYExtent.qml"; } void tst_QQuickListView2::maxExtent() { QFETCH(QString, qmlFilePath); QScopedPointer window(createView()); QVERIFY(window); window->setSource(testFileUrl(qmlFilePath)); QVERIFY2(window->status() == QQuickView::Ready, qPrintable(QDebug::toString(window->errors()))); window->resize(640, 480); window->show(); QVERIFY(QTest::qWaitForWindowExposed(window.data())); QQuickListView *view = window->rootObject()->property("view").value(); QVERIFY(view); ItemViewAccessor viewAccessor(view); if (view->orientation() == QQuickListView::Vertical) QCOMPARE(viewAccessor.maxXExtent(), 0); else if (view->orientation() == QQuickListView::Horizontal) QCOMPARE(viewAccessor.maxYExtent(), 0); } void tst_QQuickListView2::isCurrentItem_DelegateModel() { QScopedPointer window(createView()); window->setSource(testFileUrl("qtbug86744.qml")); window->resize(640, 480); window->show(); QVERIFY(QTest::qWaitForWindowExposed(window.data())); QQuickListView* listView = window->rootObject()->findChild("listView"); QVERIFY(listView); QVariant value = listView->itemAtIndex(1)->property("isCurrent"); QVERIFY(value.toBool() == true); } void tst_QQuickListView2::isCurrentItem_NoRegressionWithDelegateModelGroups() { QScopedPointer window(createView()); window->setSource(testFileUrl("qtbug98315.qml")); window->show(); QVERIFY(QTest::qWaitForWindowExposed(window.data())); QQuickListView* listView = window->rootObject()->findChild("listView"); QVERIFY(listView); QQuickItem *item3 = listView->itemAtIndex(1); QVERIFY(item3); QCOMPARE(item3->property("isCurrent").toBool(), true); QObject *item0 = listView->itemAtIndex(0); QVERIFY(item0); QCOMPARE(item0->property("isCurrent").toBool(), false); // Press left arrow key -> Item 1 should become current, Item 3 should not // be current anymore. After a previous fix of QTBUG-86744 it was working // incorrectly - see QTBUG-98315 QVERIFY(QTest::qWaitForWindowActive(window.get())); QTest::keyPress(window.get(), Qt::Key_Left); QTRY_COMPARE(item0->property("isCurrent").toBool(), true); QCOMPARE(item3->property("isCurrent").toBool(), false); } void tst_QQuickListView2::pullbackSparseList() // QTBUG_104679 { // check if PullbackHeader crashes QScopedPointer window(createView()); QVERIFY(window); window->setSource(testFileUrl("qtbug104679_header.qml")); QVERIFY2(window->status() == QQuickView::Ready, qPrintable(QDebug::toString(window->errors()))); window->resize(640, 480); window->show(); QVERIFY(QTest::qWaitForWindowExposed(window.data())); // check if PullbackFooter crashes window.reset(createView()); QVERIFY(window); window->setSource(testFileUrl("qtbug104679_footer.qml")); QVERIFY2(window->status() == QQuickView::Ready, qPrintable(QDebug::toString(window->errors()))); window->resize(640, 480); window->show(); QVERIFY(QTest::qWaitForWindowExposed(window.data())); } void tst_QQuickListView2::highlightWithBound() { QQmlEngine engine; QQmlComponent c(&engine, testFileUrl("highlightWithBound.qml")); QVERIFY2(c.isReady(), qPrintable(c.errorString())); QScopedPointer o(c.create()); QVERIFY(!o.isNull()); QQuickListView *listView = qobject_cast(o.data()); QVERIFY(listView); QQuickItem *highlight = listView->highlightItem(); QVERIFY(highlight); QCOMPARE(highlight->objectName(), QStringLiteral("highlight")); } void tst_QQuickListView2::sectionIsCompatibleWithBoundComponents() { QTest::failOnWarning(".?"); QQmlEngine engine; QQmlComponent c(&engine, testFileUrl("sectionBoundComponent.qml")); QVERIFY2(c.isReady(), qPrintable(c.errorString())); QScopedPointer o(c.create()); QVERIFY(!o.isNull()); QQuickListView *listView = qobject_cast(o.data()); QVERIFY(listView); QTRY_COMPARE(listView->currentSection(), "42"); } void tst_QQuickListView2::sectionGeometryChange() { QScopedPointer window(createView()); QTRY_VERIFY(window); window->setSource(testFileUrl("sectionGeometryChange.qml")); window->show(); QVERIFY(QTest::qWaitForWindowExposed(window.data())); QQuickListView *listview = findItem(window->rootObject(), "list"); QTRY_VERIFY(listview); QQuickItem *contentItem = listview->contentItem(); QTRY_VERIFY(contentItem); QVERIFY(QQuickTest::qWaitForPolish(listview)); QQuickItem *section1 = findItem(contentItem, "Section1"); QVERIFY(section1); QQuickItem *element1 = findItem(contentItem, "Element1"); QVERIFY(element1); QCOMPARE(element1->y(), section1->y() + section1->height()); // Update the height of the section delegate and verify that the next element is not overlapping section1->setHeight(section1->height() + 10); QTRY_COMPARE(element1->y(), section1->y() + section1->height()); } void tst_QQuickListView2::areaZeroviewDoesNotNeedlesslyPopulateWholeModel() { QTest::failOnWarning(QRegularExpression(".*")); QQmlEngine engine; QQmlComponent c(&engine, testFileUrl("areaZeroView.qml")); QVERIFY2(c.isReady(), qPrintable(c.errorString())); std::unique_ptr root(c.create()); QVERIFY(root); auto delegateCreationCounter = [&]() { return root->property("delegateCreationCounter").toInt(); }; // wait for onComplete to be settled QTRY_VERIFY(delegateCreationCounter() != 0); auto view = qobject_cast(qmlContext(root.get())->objectForName("lv")); QVERIFY(view); QCOMPARE(view->count(), 6'000); // we use 100, which is < 6000, but larger than the actual expected value // that's to give the test some leniency in case the ListView implementation // changes in the future to instantiate a few more items outside of the viewport QVERIFY(delegateCreationCounter() < 100); } void tst_QQuickListView2::delegateContextHandling() { QQmlEngine engine; QQmlComponent c(&engine, testFileUrl("delegateContextHandling.qml")); QVERIFY2(c.isReady(), qPrintable(c.errorString())); std::unique_ptr o(c.create()); QVERIFY(o); for (int i = 0; i < 10; ++i) { QQuickItem *delegate = nullptr; QMetaObject::invokeMethod(o.get(), "toggle", Q_RETURN_ARG(QQuickItem *, delegate)); QVERIFY(delegate); } } class TestFetchMoreModel : public QAbstractListModel { Q_OBJECT public: QVariant data(const QModelIndex& index, int role) const override { if (role == Qt::DisplayRole) return QString::number(index.row()); return {}; } int columnCount(const QModelIndex&) const override { return 1; } int rowCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : m_lines; } QModelIndex parent(const QModelIndex&) const override { return {}; } bool canFetchMore(const QModelIndex &) const override { return true; } void fetchMore(const QModelIndex & parent) override { if (Q_UNLIKELY(parent.isValid())) return; beginInsertRows(parent, m_lines, m_lines); m_lines++; endInsertRows(); } int m_lines = 3; }; void tst_QQuickListView2::fetchMore_data() { QTest::addColumn("reuseItems"); QTest::addColumn("cacheBuffer"); QTest::newRow("no reuseItems, default buffer") << false << -1; QTest::newRow("reuseItems, default buffer") << true << -1; QTest::newRow("no reuseItems, no buffer") << false << 0; QTest::newRow("reuseItems, no buffer") << true << 0; QTest::newRow("no reuseItems, buffer 100 px") << false << 100; QTest::newRow("reuseItems, buffer 100 px") << true << 100; } void tst_QQuickListView2::fetchMore() // QTBUG-95107 { QFETCH(bool, reuseItems); QFETCH(int, cacheBuffer); TestFetchMoreModel model; qmlRegisterSingletonInstance("org.qtproject.Test", 1, 0, "FetchMoreModel", &model); QQuickView window; QVERIFY(QQuickTest::showView(window, testFileUrl("fetchMore.qml"))); auto *listView = qobject_cast(window.rootObject()); QVERIFY(listView); listView->setReuseItems(reuseItems); if (cacheBuffer >= 0) listView->setCacheBuffer(cacheBuffer); for (int i = 0; i < 3; ++i) { const int rowCount = listView->count(); if (lcTests().isDebugEnabled()) QTest::qWait(1000); listView->flick(0, -5000); QTRY_VERIFY(!listView->isMoving()); qCDebug(lcTests) << "after flick: contentY" << listView->contentY() << "rows" << rowCount << "->" << listView->count(); QCOMPARE_GT(listView->count(), rowCount); QCOMPARE_GE(model.m_lines, listView->count()); // fetchMore() was called } } void tst_QQuickListView2::changingOrientationResetsPreviousAxisValues_data() { QTest::addColumn("sourceFile"); QTest::newRow("ObjectModel") << QByteArray("changingOrientationWithObjectModel.qml"); QTest::newRow("ListModel") << QByteArray("changingOrientationWithListModel.qml"); } void tst_QQuickListView2::changingOrientationResetsPreviousAxisValues() // QTBUG-115696 { QFETCH(QByteArray, sourceFile); QQuickView window; QVERIFY(QQuickTest::showView(window, testFileUrl(QString::fromLatin1(sourceFile)))); auto *listView = qobject_cast(window.rootObject()); QVERIFY(listView); // Starts of with vertical orientation. X should be 0 for all delegates, but not Y. QVERIFY(listView->property("isXReset").toBool()); QVERIFY(!listView->property("isYReset").toBool()); listView->setOrientation(QQuickListView::Orientation::Horizontal); // Y should be 0 for all delegates, but not X. QVERIFY(!listView->property("isXReset").toBool()); QVERIFY(listView->property("isYReset").toBool()); listView->setOrientation(QQuickListView::Orientation::Vertical); // X should be 0 for all delegates, but not Y. QVERIFY(listView->property("isXReset").toBool()); QVERIFY(!listView->property("isYReset").toBool()); } void tst_QQuickListView2::bindingDirectlyOnPositionInHeaderAndFooterDelegates_data() { QTest::addColumn("sourceFile"); QTest::addColumn("pos"); QTest::addColumn("size"); QTest::newRow("XPosition") << QByteArray("bindOnHeaderAndFooterXPosition.qml") << &QQuickItem::x << &QQuickItem::width; QTest::newRow("YPosition") << QByteArray("bindOnHeaderAndFooterYPosition.qml") << &QQuickItem::y << &QQuickItem::height; } void tst_QQuickListView2::bindingDirectlyOnPositionInHeaderAndFooterDelegates() { typedef qreal (QQuickItem::*position_func_t)() const; QFETCH(QByteArray, sourceFile); QFETCH(position_func_t, pos); QFETCH(position_func_t, size); QQuickView window; QVERIFY(QQuickTest::showView(window, testFileUrl(QString::fromLatin1(sourceFile)))); auto *listView = qobject_cast(window.rootObject()); QVERIFY(listView); const qreal widthOrHeight = (listView->*size)(); QCOMPARE((listView->headerItem()->*pos)(), (widthOrHeight - 50) / 2); QCOMPARE((listView->footerItem()->*pos)(), (widthOrHeight - 50) / 2); // Verify that the "regular" delegate items, don't honor x and y bindings. // This should only be allowed for header and footer delegates. for (int i = 0; i < listView->count(); ++i) QCOMPARE((listView->itemAtIndex(i)->*pos)(), 0); } void tst_QQuickListView2::clearObjectListModel() { QQmlEngine engine; QQmlComponent delegate(&engine); // Need one required property to trigger the incremental rebuilding of metaobjects. delegate.setData("import QtQuick\nItem { required property int index }", QUrl()); QQuickListView list; engine.setContextForObject(&list, engine.rootContext()); list.setDelegate(&delegate); list.setWidth(640); list.setHeight(480); QScopedPointer modelObject(new QObject); // Use a list that might also carry something non-QObject list.setModel(QVariantList { QVariant::fromValue(modelObject.data()), QVariant::fromValue(modelObject.data()) }); QVERIFY(list.itemAtIndex(0)); modelObject.reset(); // list should not access dangling pointer from old model data anymore. list.setModel(QVariantList()); QVERIFY(!list.itemAtIndex(0)); } void tst_QQuickListView2::gadgetModelSections() { QQuickView window; QVERIFY(QQuickTest::showView(window, testFileUrl("gadgetlist.qml"))); QQuickListView *listview = qobject_cast(window.rootObject()); QTRY_VERIFY(listview); QVERIFY(QQuickTest::qWaitForPolish(listview)); QCOMPARE(listview->count(), 4); QCOMPARE(listview->currentSection(), "small"); } void tst_QQuickListView2::visibleBoundToCountGreaterThanZero() { QQuickView window; QVERIFY(QQuickTest::showView(window, testFileUrl("visibleBoundToCountGreaterThanZero.qml"))); auto *listView = window.rootObject()->property("listView").value(); QVERIFY(listView); QSignalSpy countChangedSpy(listView, SIGNAL(countChanged())); QVERIFY(countChangedSpy.isValid()); QTRY_COMPARE_GT_WITH_TIMEOUT(listView->count(), 1, oneSecondInMs); // Using the TRY variant here as well is necessary. QTRY_COMPARE_GT_WITH_TIMEOUT(countChangedSpy.count(), 1, oneSecondInMs); QVERIFY(listView->isVisible()); } void tst_QQuickListView2::setDelegateAfterModel() { QQmlEngine engine; const QUrl url = testFileUrl("setDelegateAfterModel.qml"); QQmlComponent component(&engine, url); QVERIFY2(component.isReady(), qPrintable(component.errorString())); QScopedPointer object(component.create()); QVERIFY(!object.isNull()); // If the model was lost by setting the delegate, the count would be 0. QCOMPARE(object->property("count").toInt(), 3); QTest::ignoreMessage( QtWarningMsg, qPrintable(url.toString() + ":17:5: QML ListView: Cannot retain explicitly set " "delegate on non-DelegateModel")); object->setProperty("useObjectModel", QVariant::fromValue(true)); QCOMPARE(object->property("count").toInt(), 2); // The old model must not mess with the view anymore. QTest::failOnWarning(qPrintable(url.toString() + ":17:5: QML ListView: Explicitly set delegate " "is externally overridden")); QMetaObject::invokeMethod(object.data(), "plantDelegate"); QCOMPARE(object->property("count").toInt(), 4); } template const char *enumKey(Enum value) { const QMetaObject *mo = qt_getEnumMetaObject(value); const QMetaEnum metaEnum = mo->enumerator(mo->indexOfEnumerator(qt_getEnumName(value))); return metaEnum.valueToKey(value); } void tst_QQuickListView2::delegateModelAccess_data() { QTest::addColumn("access"); QTest::addColumn("modelKind"); QTest::addColumn("delegateKind"); using Access = QQmlDelegateModel::DelegateModelAccess; for (auto access : { Access::Qt5ReadWrite, Access::ReadOnly, Access::ReadWrite }) { for (auto model : { Model::Singular, Model::List, Model::Array, Model::Object }) { for (auto delegate : { Delegate::Untyped, Delegate::Typed }) { QTest::addRow("%s-%s-%s", enumKey(access), enumKey(model), enumKey(delegate)) << access << model << delegate; } } } } void tst_QQuickListView2::delegateModelAccess() { QFETCH(QQmlDelegateModel::DelegateModelAccess, access); QFETCH(Model::Kind, modelKind); QFETCH(Delegate::Kind, delegateKind); QQmlEngine engine; const QUrl url = testFileUrl("delegateModelAccess.qml"); QQmlComponent c(&engine, url); QVERIFY2(c.isReady(), qPrintable(c.errorString())); QScopedPointer object(c.create()); QQuickListView *listView = qobject_cast(object.data()); QVERIFY(listView); QSignalSpy modelChangedSpy(listView, &QQuickItemView::modelChanged); if (delegateKind == Delegate::Untyped && modelKind == Model::Array) QSKIP("Properties of objects in arrays are not exposed as context properties"); if (access == QQmlDelegateModel::ReadOnly) { const QRegularExpression message( url.toString() + ":[0-9]+: TypeError: Cannot assign to read-only property \"a\""); QTest::ignoreMessage(QtWarningMsg, message); if (delegateKind == Delegate::Untyped) QTest::ignoreMessage(QtWarningMsg, message); } object->setProperty("delegateModelAccess", access); object->setProperty("modelIndex", modelKind); object->setProperty("delegateIndex", delegateKind); listView->setCurrentIndex(0); QObject *delegate = listView->itemAtIndex(0); QVERIFY(delegate); const bool modelWritable = access != QQmlDelegateModel::ReadOnly; const bool immediateWritable = (delegateKind == Delegate::Untyped) ? access != QQmlDelegateModel::ReadOnly : access == QQmlDelegateModel::ReadWrite; // Only the array is actually updated itself. The other models are pointers const bool writeShouldSignal = modelKind == Model::Kind::Array; double expected = 11; // Initial setting of the model, signals one update int expectedModelUpdates = 1; QCOMPARE(modelChangedSpy.count(), expectedModelUpdates); QCOMPARE(delegate->property("immediateX").toDouble(), expected); QCOMPARE(delegate->property("modelX").toDouble(), expected); if (modelWritable) { expected = 3; if (writeShouldSignal) ++expectedModelUpdates; } QMetaObject::invokeMethod(delegate, "writeThroughModel"); QCOMPARE(delegate->property("immediateX").toDouble(), expected); QCOMPARE(delegate->property("modelX").toDouble(), expected); QCOMPARE(modelChangedSpy.count(), expectedModelUpdates); if (immediateWritable) { expected = 1; if (writeShouldSignal) ++expectedModelUpdates; } QMetaObject::invokeMethod(delegate, "writeImmediate"); // Writes to required properties always succeed, but might not be propagated to the model QCOMPARE(delegate->property("immediateX").toDouble(), delegateKind == Delegate::Untyped ? expected : 1); QCOMPARE(delegate->property("modelX").toDouble(), expected); QCOMPARE(modelChangedSpy.count(), expectedModelUpdates); } enum RemovalPolicy { RemoveAndDestroy, OnlyDestroy }; void tst_QQuickListView2::removeAndDestroyObjectModelItem_data() { QTest::addColumn("qmlFilePath"); QTest::addColumn("removalPolicy"); QTest::addRow("remove and destroy, no transitions") << "removeAndDestroyObjectModelItem.qml" << RemoveAndDestroy; QTest::addRow("destroy, no transitions") << "removeAndDestroyObjectModelItem.qml" << OnlyDestroy; QTest::addRow("remove and destroy, transitions") << "removeAndDestroyObjectModelItemWithTransitions.qml" << RemoveAndDestroy; QTest::addRow("destroy, transitions") << "removeAndDestroyObjectModelItemWithTransitions.qml" << OnlyDestroy; } // QTBUG-46798, QTBUG-133256 void tst_QQuickListView2::removeAndDestroyObjectModelItem() { QFETCH(QString, qmlFilePath); QFETCH(RemovalPolicy, removalPolicy); QQuickView window; QVERIFY(QQuickTest::showView(window, testFileUrl(qmlFilePath))); QQuickListView *listView = qobject_cast(window.rootObject()); QVERIFY(listView); auto *objectModel = listView->model().value(); QVERIFY(objectModel); QPointer firstItem = objectModel->get(0); QVERIFY(firstItem); // Shouldn't crash. if (removalPolicy == RemoveAndDestroy) objectModel->remove(0); firstItem->deleteLater(); QTRY_VERIFY(!firstItem); // Now try moving the view. It also shouldn't crash. if (removalPolicy == RemoveAndDestroy) { if (QQuickTest::qIsPolishScheduled(listView)) QVERIFY(QQuickTest::qWaitForPolish(listView)); } listView->positionViewAtEnd(); } QTEST_MAIN(tst_QQuickListView2) #include "tst_qquicklistview2.moc"