diff options
Diffstat (limited to 'tests/auto/quick/qquicklistview2/tst_qquicklistview2.cpp')
-rw-r--r-- | tests/auto/quick/qquicklistview2/tst_qquicklistview2.cpp | 1255 |
1 files changed, 1255 insertions, 0 deletions
diff --git a/tests/auto/quick/qquicklistview2/tst_qquicklistview2.cpp b/tests/auto/quick/qquicklistview2/tst_qquicklistview2.cpp new file mode 100644 index 0000000000..bdac2112b6 --- /dev/null +++ b/tests/auto/quick/qquicklistview2/tst_qquicklistview2.cpp @@ -0,0 +1,1255 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include <QtTest/QtTest> +#include <QtQuick/qquickview.h> +#include <QtQuick/private/qquickitemview_p_p.h> +#include <QtQuick/private/qquicklistview_p.h> +#include <QtQuickTest/QtQuickTest> +#include <QStringListModel> +#include <QQmlApplicationEngine> + +#include <QtQuickTestUtils/private/viewtestutils_p.h> +#include <QtQuickTestUtils/private/visualtestutils_p.h> +#include <QtQuickTestUtils/private/qmlutils_p.h> + +Q_LOGGING_CATEGORY(lcTests, "qt.quick.tests") + +using namespace QQuickViewTestUtils; +using namespace QQuickVisualTestUtils; + +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 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(); + +private: + void flickWithTouch(QQuickWindow *window, const QPoint &from, const QPoint &to); + QScopedPointer<QPointingDevice> touchDevice = QScopedPointer<QPointingDevice>(QTest::createTouchDevice()); +}; + +tst_QQuickListView2::tst_QQuickListView2() + : QQmlDataTest(QT_QMLTEST_DATADIR) +{ +} + +void tst_QQuickListView2::urlListModel() +{ + QScopedPointer<QQuickView> window(createView()); + QVERIFY(window); + + QList<QUrl> 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<QQuickListView*>(); + 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<QQuickItemView::LayoutDirection>("layoutDirection"); + + for (int layDir = QQuickItemView::LeftToRight; layDir <= (int)QQuickItemView::VerticalBottomToTop; layDir++) { + const char *enumValueName = QMetaEnum::fromType<QQuickItemView::LayoutDirection>().valueToKey(layDir); + QTest::newRow(enumValueName) << static_cast<QQuickItemView::LayoutDirection>(layDir); + } +} + +void tst_QQuickListView2::viewportAvoidUndesiredMovementOnSetCurrentIndex() +{ + QScopedPointer<QQuickView> 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<QQuickListView>(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<QQuickView> window(createView()); + QVERIFY(window); + window->setFlag(Qt::FramelessWindowHint); + window->setSource(testFileUrl("delegateWithMouseArea.qml")); + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window.data())); + + QQuickListView *listview = findItem<QQuickListView>(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<Qt::LayoutDirection>(layoutDirection)); + else + listview->setVerticalLayoutDirection(static_cast<QQuickItemView::VerticalLayoutDirection>(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<QQuickListView*>(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<QQuickView> window(createView()); + QTRY_VERIFY(window); + window->setSource(testFileUrl("qtbug_92809.qml")); + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window.data())); + + QQuickListView *listview = findItem<QQuickListView>(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<QQuickView> window(createView()); + QTRY_VERIFY(window); + window->setSource(testFileUrl("footerUpdate.qml")); + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window.data())); + + QQuickListView *listview = findItem<QQuickListView>(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<QQuickView> window(createView()); + QTRY_VERIFY(window); + window->setSource(testFileUrl("sectionsNoOverlap.qml")); + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window.data())); + + QQuickListView *listview = findItem<QQuickListView>(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<QQuickItem>(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<QQuickItem>( + 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<QObject> o(c.create()); + QVERIFY(!o.isNull()); + QStringList strings = qvariant_cast<QStringList>(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<QObject> o(c.create()); + QVERIFY(!o.isNull()); + + QObject *delegateModel = qmlContext(o.data())->objectForName("displayDelegateModel"); + QVERIFY(delegateModel); + + QObject *items = qvariant_cast<QObject *>(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<QObject> o(component.create()); + QVERIFY2(!o.isNull(), qPrintable(component.errorString())); + + QQuickListView *a = qobject_cast<QQuickListView *>( + 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<QObject> o(c.create()); + QVERIFY(!o.isNull()); + + QQmlContext *context = qmlContext(o.data()); + + QObject *inner = context->objectForName(QLatin1String("listView")); + QVERIFY(inner != nullptr); + QQuickListView *listView = qobject_cast<QQuickListView *>(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<QQuickListView *>(inner2); + QVERIFY(listView2 != nullptr); + QObject *item2 = listView2->itemAtIndex(0); + QVERIFY(item2); + QCOMPARE(item2->objectName(), QLatin1String("fooouter0")); + + QQmlComponent *comp = qobject_cast<QQmlComponent *>( + 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<QObject> outerItem(comp->create(context)); + QVERIFY(!outerItem.isNull()); + QQuickListView *innerListView = qobject_cast<QQuickListView *>( + 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<QByteArray>("qmlFile"); + QTest::addColumn<QQuickFlickable::BoundsBehavior>("boundsBehavior"); + QTest::addColumn<bool>("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<QQuickListView*>(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 + // @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, touchDevice.data()).press(0, {100, 100}); + QQuickTouchUtils::flush(&window); + QTest::touchEvent(&window, touchDevice.data()).release(0, {100, 100}); + QQuickTouchUtils::flush(&window); + + 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; + + // which delegate received the second press, during flicking? + const int lastPressed = pressedDelegates.last().toInt(); + QVERIFY(lastPressed > 5); + QCOMPARE(releasedDelegates.last(), lastPressed); + QCOMPARE(tappedDelegates.last(), lastPressed); + QCOMPARE(canceledDelegates.size(), expectCanceled ? 1 : 0); // only the first press was canceled, not the second +} + +void tst_QQuickListView2::flickDuringFlicking_data() +{ + QTest::addColumn<QByteArray>("qmlFile"); + QTest::addColumn<QQuickFlickable::BoundsBehavior>("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<QQuickListView*>(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, touchDevice.data()).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, touchDevice.data()).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, touchDevice.data()).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, touchDevice.data()).release(0, {100, 100}); +} + +void tst_QQuickListView2::flickWithTouch(QQuickWindow *window, const QPoint &from, const QPoint &to) +{ + QTest::touchEvent(window, touchDevice.data()).press(0, from, window); + QQuickTouchUtils::flush(window); + + QPoint diff = to - from; + for (int i = 1; i <= 8; ++i) { + QTest::touchEvent(window, touchDevice.data()).move(0, from + i * diff / 8, window); + QQuickTouchUtils::flush(window); + } + QTest::touchEvent(window, touchDevice.data()).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<SingletonModel>("test", 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<QQuickView> 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<QQuickListView *>(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<QQuickListView::Orientation>("orientation"); + QTest::addColumn<Qt::LayoutDirection>("layoutDirection"); + QTest::addColumn<QQuickItemView::VerticalLayoutDirection>("verticalLayoutDirection"); + QTest::addColumn<QQuickItemView::HighlightRangeMode>("highlightRangeMode"); + QTest::addColumn<QPoint>("forwardAngleDelta"); + QTest::addColumn<qreal>("snapAlignment"); + QTest::addColumn<qreal>("endExtent"); + QTest::addColumn<qreal>("startExtent"); + QTest::addColumn<qreal>("preferredHighlightBegin"); + QTest::addColumn<qreal>("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; +} + +class FriendlyItemView : public QQuickItemView +{ + friend class ItemViewAccessor; +}; + +class ItemViewAccessor +{ +public: + ItemViewAccessor(QQuickItemView *itemView) : + mItemView(reinterpret_cast<FriendlyItemView*>(itemView)) + { + } + + qreal maxXExtent() const + { + return mItemView->maxXExtent(); + } + + qreal maxYExtent() const + { + return mItemView->maxYExtent(); + } + +private: + FriendlyItemView *mItemView = nullptr; +}; + +void tst_QQuickListView2::maxExtent_data() +{ + QTest::addColumn<QString>("qmlFilePath"); + QTest::addRow("maxXExtent") << "maxXExtent.qml"; + QTest::addRow("maxYExtent") << "maxYExtent.qml"; +} + +void tst_QQuickListView2::maxExtent() +{ + QFETCH(QString, qmlFilePath); + + QScopedPointer<QQuickView> 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<QQuickListView*>(); + 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<QQuickView> window(createView()); + window->setSource(testFileUrl("qtbug86744.qml")); + window->resize(640, 480); + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window.data())); + QQuickListView* listView = window->rootObject()->findChild<QQuickListView*>("listView"); + QVERIFY(listView); + QVariant value = listView->itemAtIndex(1)->property("isCurrent"); + QVERIFY(value.toBool() == true); +} + +void tst_QQuickListView2::isCurrentItem_NoRegressionWithDelegateModelGroups() +{ + QScopedPointer<QQuickView> window(createView()); + window->setSource(testFileUrl("qtbug98315.qml")); + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window.data())); + QQuickListView* listView = window->rootObject()->findChild<QQuickListView*>("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 + 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<QQuickView> 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<QObject> o(c.create()); + QVERIFY(!o.isNull()); + QQuickListView *listView = qobject_cast<QQuickListView *>(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<QObject> o(c.create()); + QVERIFY(!o.isNull()); + QQuickListView *listView = qobject_cast<QQuickListView *>(o.data()); + QVERIFY(listView); + QTRY_COMPARE(listView->currentSection(), "42"); +} + +void tst_QQuickListView2::sectionGeometryChange() +{ + QScopedPointer<QQuickView> window(createView()); + QTRY_VERIFY(window); + window->setSource(testFileUrl("sectionGeometryChange.qml")); + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window.data())); + + QQuickListView *listview = findItem<QQuickListView>(window->rootObject(), "list"); + QTRY_VERIFY(listview); + + QQuickItem *contentItem = listview->contentItem(); + QTRY_VERIFY(contentItem); + QVERIFY(QQuickTest::qWaitForPolish(listview)); + + QQuickItem *section1 = findItem<QQuickItem>(contentItem, "Section1"); + QVERIFY(section1); + QQuickItem *element1 = findItem<QQuickItem>(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<QObject> 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<QQuickListView *>(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<QObject> 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<bool>("reuseItems"); + QTest::addColumn<int>("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<QQuickListView*>(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<QByteArray>("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<QQuickListView *>(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<QByteArray>("sourceFile"); + QTest::addColumn<qreal(QQuickItem::*)()const>("pos"); + QTest::addColumn<qreal(QQuickItem::*)()const>("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<QQuickListView *>(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)); +} + +QTEST_MAIN(tst_QQuickListView2) + +#include "tst_qquicklistview2.moc" |