aboutsummaryrefslogtreecommitdiffstats
path: root/tests/auto/quick/qquicklistview2
diff options
context:
space:
mode:
Diffstat (limited to 'tests/auto/quick/qquicklistview2')
-rw-r--r--tests/auto/quick/qquicklistview2/BLACKLIST5
-rw-r--r--tests/auto/quick/qquicklistview2/CMakeLists.txt46
-rw-r--r--tests/auto/quick/qquicklistview2/data/areaZeroView.qml22
-rw-r--r--tests/auto/quick/qquicklistview2/data/bindOnHeaderAndFooterXPosition.qml29
-rw-r--r--tests/auto/quick/qquicklistview2/data/bindOnHeaderAndFooterYPosition.qml29
-rw-r--r--tests/auto/quick/qquicklistview2/data/boundDelegateComponent.qml64
-rw-r--r--tests/auto/quick/qquicklistview2/data/buttonDelegate.qml27
-rw-r--r--tests/auto/quick/qquicklistview2/data/changingOrientationWithListModel.qml47
-rw-r--r--tests/auto/quick/qquicklistview2/data/changingOrientationWithObjectModel.qml54
-rw-r--r--tests/auto/quick/qquicklistview2/data/delegateChooserEnumRole.qml41
-rw-r--r--tests/auto/quick/qquicklistview2/data/delegateContextHandling.qml75
-rw-r--r--tests/auto/quick/qquicklistview2/data/delegateModelRefresh.qml53
-rw-r--r--tests/auto/quick/qquicklistview2/data/delegateWithMouseArea.qml73
-rw-r--r--tests/auto/quick/qquicklistview2/data/fetchMore.qml21
-rw-r--r--tests/auto/quick/qquicklistview2/data/footerUpdate.qml39
-rw-r--r--tests/auto/quick/qquicklistview2/data/highlightWithBound.qml10
-rw-r--r--tests/auto/quick/qquicklistview2/data/innerRequired.qml35
-rw-r--r--tests/auto/quick/qquicklistview2/data/maxXExtent.qml29
-rw-r--r--tests/auto/quick/qquicklistview2/data/maxYExtent.qml30
-rw-r--r--tests/auto/quick/qquicklistview2/data/metaSequenceAsModel.qml14
-rw-r--r--tests/auto/quick/qquicklistview2/data/mouseAreaDelegate.qml30
-rw-r--r--tests/auto/quick/qquicklistview2/data/noCrashOnIndexChange.qml48
-rw-r--r--tests/auto/quick/qquicklistview2/data/qtbug104679_footer.qml21
-rw-r--r--tests/auto/quick/qquicklistview2/data/qtbug104679_header.qml21
-rw-r--r--tests/auto/quick/qquicklistview2/data/qtbug86744.qml25
-rw-r--r--tests/auto/quick/qquicklistview2/data/qtbug98315.qml98
-rw-r--r--tests/auto/quick/qquicklistview2/data/qtbug_92809.qml71
-rw-r--r--tests/auto/quick/qquicklistview2/data/sectionBoundComponent.qml14
-rw-r--r--tests/auto/quick/qquicklistview2/data/sectionGeometryChange.qml58
-rw-r--r--tests/auto/quick/qquicklistview2/data/sectionsNoOverlap.qml77
-rw-r--r--tests/auto/quick/qquicklistview2/data/singletonModelLifetime.qml32
-rw-r--r--tests/auto/quick/qquicklistview2/data/snapOneItem.qml34
-rw-r--r--tests/auto/quick/qquicklistview2/data/urlListModel.qml22
-rw-r--r--tests/auto/quick/qquicklistview2/data/viewportAvoidUndesiredMovementOnSetCurrentIndex.qml47
-rw-r--r--tests/auto/quick/qquicklistview2/tst_qquicklistview2.cpp1255
-rw-r--r--tests/auto/quick/qquicklistview2/typerolemodel.cpp41
-rw-r--r--tests/auto/quick/qquicklistview2/typerolemodel.h30
37 files changed, 2667 insertions, 0 deletions
diff --git a/tests/auto/quick/qquicklistview2/BLACKLIST b/tests/auto/quick/qquicklistview2/BLACKLIST
new file mode 100644
index 0000000000..0162bbc852
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/BLACKLIST
@@ -0,0 +1,5 @@
+[tapDelegateDuringFlicking]
+android # QTBUG-104471
+[flickDuringFlicking]
+android # QTBUG-104471
+macos ci # QTBUG-105190
diff --git a/tests/auto/quick/qquicklistview2/CMakeLists.txt b/tests/auto/quick/qquicklistview2/CMakeLists.txt
new file mode 100644
index 0000000000..faa86ce733
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/CMakeLists.txt
@@ -0,0 +1,46 @@
+# Copyright (C) 2022 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT)
+ cmake_minimum_required(VERSION 3.16)
+ project(tst_qquicklistview2 LANGUAGES CXX)
+ find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST)
+endif()
+
+# Collect test data
+file(GLOB_RECURSE test_data_glob
+ RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}
+ data/*)
+list(APPEND test_data ${test_data_glob})
+
+qt_internal_add_test(tst_qquicklistview2
+ SOURCES
+ typerolemodel.h typerolemodel.cpp
+ tst_qquicklistview2.cpp
+ LIBRARIES
+ Qt::CorePrivate
+ Qt::Gui
+ Qt::GuiPrivate
+ Qt::QmlModelsPrivate
+ Qt::QmlPrivate
+ Qt::QuickPrivate
+ Qt::QuickTest
+ Qt::QuickTestUtilsPrivate
+ TESTDATA ${test_data}
+)
+
+qt_policy(SET QTP0001 NEW)
+
+qt6_add_qml_module(tst_qquicklistview2
+ URI Test
+)
+
+qt_internal_extend_target(tst_qquicklistview2 CONDITION ANDROID OR IOS
+ DEFINES
+ QT_QMLTEST_DATADIR=":/data"
+)
+
+qt_internal_extend_target(tst_qquicklistview2 CONDITION NOT ANDROID AND NOT IOS
+ DEFINES
+ QT_QMLTEST_DATADIR="${CMAKE_CURRENT_SOURCE_DIR}/data"
+)
diff --git a/tests/auto/quick/qquicklistview2/data/areaZeroView.qml b/tests/auto/quick/qquicklistview2/data/areaZeroView.qml
new file mode 100644
index 0000000000..e0329f4e83
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/areaZeroView.qml
@@ -0,0 +1,22 @@
+import QtQuick
+
+Window {
+ width: 600
+ height: 600
+ visible: true
+ property int delegateCreationCounter: 0
+
+ ListView {
+ id: lv
+ anchors.fill: parent
+ model: 6000
+
+ delegate: Rectangle {
+ width: ListView.view.width
+ height: ListView.view.width / 6
+ color: "white"
+ border.width: 1
+ Component.onCompleted: ++delegateCreationCounter
+ }
+ }
+}
diff --git a/tests/auto/quick/qquicklistview2/data/bindOnHeaderAndFooterXPosition.qml b/tests/auto/quick/qquicklistview2/data/bindOnHeaderAndFooterXPosition.qml
new file mode 100644
index 0000000000..69431fb525
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/bindOnHeaderAndFooterXPosition.qml
@@ -0,0 +1,29 @@
+import QtQuick
+
+ListView {
+ width: 200
+ height: 300
+ spacing: 20
+ orientation: ListView.Vertical
+
+ header: Rectangle {
+ x: (ListView.view.width - width) / 2
+ color: 'tomato'
+ width: 50
+ height: 50
+ }
+
+ footer: Rectangle {
+ x: (ListView.view.width - width) / 2
+ color: 'lime'
+ width: 50
+ height: 50
+ }
+
+ model: 3
+ delegate: Text {
+ text: 'Foobar'
+ horizontalAlignment: Text.AlignHCenter
+ width: ListView.view.width
+ }
+}
diff --git a/tests/auto/quick/qquicklistview2/data/bindOnHeaderAndFooterYPosition.qml b/tests/auto/quick/qquicklistview2/data/bindOnHeaderAndFooterYPosition.qml
new file mode 100644
index 0000000000..a484a154a7
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/bindOnHeaderAndFooterYPosition.qml
@@ -0,0 +1,29 @@
+import QtQuick
+
+ListView {
+ width: 300
+ height: 200
+ spacing: 20
+ orientation: ListView.Horizontal
+
+ header: Rectangle {
+ y: (ListView.view.height - height) / 2
+ color: 'tomato'
+ width: 50
+ height: 50
+ }
+
+ footer: Rectangle {
+ y: (ListView.view.height - height) / 2
+ color: 'lime'
+ width: 50
+ height: 50
+ }
+
+ model: 3
+ delegate: Text {
+ text: 'Foobar'
+ verticalAlignment: Text.AlignVCenter
+ height: ListView.view.height
+ }
+}
diff --git a/tests/auto/quick/qquicklistview2/data/boundDelegateComponent.qml b/tests/auto/quick/qquicklistview2/data/boundDelegateComponent.qml
new file mode 100644
index 0000000000..5e4de1a08d
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/boundDelegateComponent.qml
@@ -0,0 +1,64 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+Item {
+ id: outer
+ objectName: "outer"
+ ListView {
+ id: listView
+ width: 100
+ height: 100
+ model: 1
+ property string foo: "foo"
+ delegate: Text {
+ property var notThere: index
+ objectName: listView.foo + outer.objectName + notThere
+ }
+ }
+
+ ListView {
+ id: listView2
+ width: 100
+ height: 100
+ model: 1
+ delegate: Text {
+ required property int index
+ objectName: listView.foo + outer.objectName + index
+ }
+ }
+
+ Component {
+ id: outerComponent
+ Item {
+ ListModel {
+ id: listModel
+ ListElement {
+ myColor: "red"
+ }
+
+ ListElement {
+ myColor: "green"
+ }
+
+ ListElement {
+ myColor: "blue"
+ }
+ }
+
+ Component {
+ id: innerComponent
+ Rectangle {
+ objectName: model.myColor
+ }
+ }
+
+ ListView {
+ width: 100
+ height: 100
+ id: innerListView
+ model: listModel
+ delegate: innerComponent
+ }
+ }
+ }
+}
diff --git a/tests/auto/quick/qquicklistview2/data/buttonDelegate.qml b/tests/auto/quick/qquicklistview2/data/buttonDelegate.qml
new file mode 100644
index 0000000000..a40ba1cd7e
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/buttonDelegate.qml
@@ -0,0 +1,27 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+
+ListView {
+ id: root
+ width: 320
+ height: 480
+ model: 100
+
+ property var pressedDelegates: []
+ property var releasedDelegates: []
+ property var tappedDelegates: []
+ property var canceledDelegates: []
+
+ delegate: Button {
+ required property int index
+ objectName: text
+ text: "button " + index
+ height: 100
+ width: 320
+
+ onPressed: root.pressedDelegates.push(index)
+ onReleased: root.releasedDelegates.push(index)
+ onClicked: root.tappedDelegates.push(index)
+ onCanceled: root.canceledDelegates.push(index)
+ }
+}
diff --git a/tests/auto/quick/qquicklistview2/data/changingOrientationWithListModel.qml b/tests/auto/quick/qquicklistview2/data/changingOrientationWithListModel.qml
new file mode 100644
index 0000000000..366b20b029
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/changingOrientationWithListModel.qml
@@ -0,0 +1,47 @@
+import QtQuick
+
+ListView {
+ id: root
+
+ function allDelegates(valueSelector) {
+ let sum = 0;
+ for (let i = 0; i < root.count; i++)
+ sum += valueSelector(root.itemAtIndex(i));
+ return sum;
+ }
+
+ readonly property bool isXReset: allDelegates(function(item) { return item?.x ?? 0; }) === 0
+ readonly property bool isYReset: allDelegates(function(item) { return item?.y ?? 0; }) === 0
+
+ width: 500
+ height: 500
+ delegate: Rectangle {
+ width: root.width
+ height: root.height
+ color: c
+ }
+ model: ListModel {
+ ListElement {
+ c: "red"
+ }
+ ListElement {
+ c: "green"
+ }
+ ListElement {
+ c: "blue"
+ }
+ ListElement {
+ c: "cyan"
+ }
+ ListElement {
+ c: "magenta"
+ }
+ ListElement {
+ c: "teal"
+ }
+ }
+ clip: true
+ orientation: ListView.Vertical
+ snapMode: ListView.SnapOneItem
+ highlightRangeMode: ListView.StrictlyEnforceRange
+} \ No newline at end of file
diff --git a/tests/auto/quick/qquicklistview2/data/changingOrientationWithObjectModel.qml b/tests/auto/quick/qquicklistview2/data/changingOrientationWithObjectModel.qml
new file mode 100644
index 0000000000..1d165a496e
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/changingOrientationWithObjectModel.qml
@@ -0,0 +1,54 @@
+import QtQuick
+
+ListView {
+ id: root
+
+ readonly property bool isXReset: red.x === 0 && green.x === 0 && blue.x === 0 && cyan.x === 0 && magenta.x === 0 && teal.x === 0
+
+ readonly property bool isYReset: red.y === 0 && green.y === 0 && blue.y === 0 && cyan.y === 0 && magenta.y === 0 && teal.y === 0
+
+ width: 500
+ height: 500
+ model: ObjectModel {
+ Rectangle {
+ id: red
+ width: root.width
+ height: root.height
+ color: "red"
+ }
+ Rectangle {
+ id: green
+ width: root.width
+ height: root.height
+ color: "green"
+ }
+ Rectangle {
+ id: blue
+ width: root.width
+ height: root.height
+ color: "blue"
+ }
+ Rectangle {
+ id: cyan
+ width: root.width
+ height: root.height
+ color: "cyan"
+ }
+ Rectangle {
+ id: magenta
+ width: root.width
+ height: root.height
+ color: "magenta"
+ }
+ Rectangle {
+ id: teal
+ width: root.width
+ height: root.height
+ color: "teal"
+ }
+ }
+ clip: true
+ orientation: ListView.Vertical
+ snapMode: ListView.SnapOneItem
+ highlightRangeMode: ListView.StrictlyEnforceRange
+} \ No newline at end of file
diff --git a/tests/auto/quick/qquicklistview2/data/delegateChooserEnumRole.qml b/tests/auto/quick/qquicklistview2/data/delegateChooserEnumRole.qml
new file mode 100644
index 0000000000..66e92c5616
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/delegateChooserEnumRole.qml
@@ -0,0 +1,41 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+import Qt.labs.qmlmodels
+import Test
+
+ListView {
+ width: 300; height: 300
+ model: TypeRoleModel {}
+ delegate: DelegateChooser {
+ role: "type"
+ DelegateChoice {
+ roleValue: 0
+ Text {
+ property int delegateType: 0
+ text: model.text + " of type " + model.type
+ }
+ }
+ DelegateChoice {
+ roleValue: "Markdown"
+ Text {
+ property int delegateType: 1
+ text: model.text + " of **type** " + model.type
+ textFormat: Text.MarkdownText
+ }
+ }
+ DelegateChoice {
+ roleValue: TypeRoleModel.Rect
+ Rectangle {
+ property int delegateType: 2
+ width: 300; height: 20
+ color: "wheat"
+ Text {
+ text: model.text + " of type " + model.type
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+ }
+ }
+}
diff --git a/tests/auto/quick/qquicklistview2/data/delegateContextHandling.qml b/tests/auto/quick/qquicklistview2/data/delegateContextHandling.qml
new file mode 100644
index 0000000000..4c513df905
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/delegateContextHandling.qml
@@ -0,0 +1,75 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Window
+
+Item {
+ id: win
+ height: 640
+ width: 480
+
+ property string currentModel: 'foo'
+
+ function toggle() : Item {
+ var ret = listView.itemAtIndex(0);
+ win.currentModel = win.currentModel === 'foo' ? 'bar' : 'foo'
+
+ switch (win.currentModel) {
+ case 'foo':
+ if (listView.model) {
+ listView.model.destroy()
+ }
+ listView.model = fooModelComponent.createObject(win)
+ break
+
+ case 'bar':
+ if (listView.model) {
+ listView.model.destroy()
+ }
+ listView.model = barModelComponent.createObject(win)
+ break
+ }
+
+ return ret;
+ }
+
+ Component {
+ id: fooModelComponent
+ ListModel {
+ ListElement { textValue: "foo1" }
+ }
+ }
+
+ Component {
+ id: barModelComponent
+ ListModel {
+ ListElement { textValue: "bar1" }
+ }
+ }
+
+ ListView {
+ states: [
+ State {
+ when: win.currentModel === 'bar'
+ PropertyChanges {
+ listView.section.property: 'sectionProp'
+ }
+ }
+ ]
+
+ id: listView
+ model: fooModelComponent.createObject(win)
+ anchors.fill: parent
+
+ section.delegate: Text {
+ required property string section
+ text: section
+ }
+
+ delegate: Text {
+ id: delg
+ text: delg.textValue
+ required property string textValue
+ }
+ }
+}
diff --git a/tests/auto/quick/qquicklistview2/data/delegateModelRefresh.qml b/tests/auto/quick/qquicklistview2/data/delegateModelRefresh.qml
new file mode 100644
index 0000000000..7a22f1a0f4
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/delegateModelRefresh.qml
@@ -0,0 +1,53 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+import QtQuick.Controls
+import QtQml.Models
+
+ApplicationWindow {
+ id: window
+ visible: true
+ width: 400
+ height: 800
+ property bool done: false
+
+ ListView {
+ model: delegateModel
+ anchors.fill: parent
+ }
+
+ DelegateModel {
+ id: delegateModel
+ model: ListModel {
+ ListElement {
+ available: true
+ }
+ ListElement {
+ available: true
+ }
+ ListElement {
+ available: true
+ }
+ }
+
+ Component.onCompleted: {
+ delegateModel.refresh()
+ done = true;
+ }
+ function refresh() {
+ var rowCount = delegateModel.model.count;
+ const flatItemsList = []
+ for (var i = 0; i < rowCount; i++) {
+ var entry = delegateModel.model.get(i);
+ flatItemsList.push(entry)
+ }
+
+ for (i = 0; i < flatItemsList.length; ++i) {
+ var item = flatItemsList[i]
+ if (item !== null)
+ items.insert(item)
+ }
+ }
+ }
+}
diff --git a/tests/auto/quick/qquicklistview2/data/delegateWithMouseArea.qml b/tests/auto/quick/qquicklistview2/data/delegateWithMouseArea.qml
new file mode 100644
index 0000000000..f447f913e6
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/delegateWithMouseArea.qml
@@ -0,0 +1,73 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+
+Rectangle {
+
+ width: 240
+ height: 320
+ color: "#ffffff"
+
+ Component {
+ id: myDelegate
+ Rectangle {
+ id: wrapper
+ width: list.orientation == ListView.Vertical ? 240 : 20
+ height: list.orientation == ListView.Vertical ? 20 : 240
+ border.width: 1
+ border.color: "black"
+ MouseArea {
+ anchors.fill: parent
+ }
+ Text {
+ text: index + ":" + (list.orientation == ListView.Vertical ? parent.y : parent.x).toFixed(0)
+ }
+ color: ListView.isCurrentItem ? "lightsteelblue" : "white"
+ }
+ }
+
+ ListView {
+ id: list
+ objectName: "list"
+ focus: true
+ width: 240
+ height: 200
+ clip: true
+ model: 30
+ headerPositioning: ListView.OverlayHeader
+ delegate: myDelegate
+
+ header: Rectangle {
+ width: list.orientation == Qt.Vertical ? 240 : 30
+ height: list.orientation == Qt.Vertical ? 30 : 240
+ color: "green"
+ z: 11
+ Text {
+ anchors.centerIn: parent
+ text: "header " + (list.orientation == ListView.Vertical ? parent.y : parent.x).toFixed(1)
+ }
+ }
+ }
+
+ // debug
+ Rectangle {
+ color: "#40ff0000"
+ border.width: txt.x
+ border.color: "black"
+ radius: 5
+ width: txt.implicitWidth + 50
+ height: txt.implicitHeight + 2 * txt.x
+ anchors.bottom: parent.bottom
+ anchors.right: parent.right
+ anchors.left: parent.left
+
+ Text {
+ id: txt
+ x: 3
+ y: x
+ text: "header position: " + (list.orientation == ListView.Vertical ? list.headerItem.y : list.headerItem.x).toFixed(1)
+ + "\ncontent position: " + (list.orientation == ListView.Vertical ? list.contentY : list.contentX).toFixed(1)
+ }
+ }
+}
diff --git a/tests/auto/quick/qquicklistview2/data/fetchMore.qml b/tests/auto/quick/qquicklistview2/data/fetchMore.qml
new file mode 100644
index 0000000000..4ce53e4d28
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/fetchMore.qml
@@ -0,0 +1,21 @@
+import QtQuick
+import org.qtproject.Test
+
+ListView {
+ id: listView
+ width: 300
+ height: 150
+ flickDeceleration: 10000
+
+ model: FetchMoreModel
+ delegate: Text {
+ height: 50
+ text: model.display
+ }
+
+ Text {
+ anchors.right: parent.right
+ text: "count " + listView.count
+ color: listView.moving ? "red" : "blue"
+ }
+}
diff --git a/tests/auto/quick/qquicklistview2/data/footerUpdate.qml b/tests/auto/quick/qquicklistview2/data/footerUpdate.qml
new file mode 100644
index 0000000000..c5729ad633
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/footerUpdate.qml
@@ -0,0 +1,39 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+
+Rectangle {
+ id: root
+ width: 800
+ height: 800
+ Component.onCompleted: { list.model.remove(0); }
+ ListView {
+ id: list
+ objectName: "list"
+ anchors.fill: parent
+ model: ListModel {
+ ListElement {
+ txt: "Foo"
+ }
+ }
+ delegate: Rectangle {
+ id: myDelegate
+ color: "red"
+ width: 800
+ height: 100
+ ListView.onRemove: SequentialAnimation {
+ PropertyAction { target: myDelegate; property: "ListView.delayRemove"; value: true }
+ NumberAnimation { target: myDelegate; property: "scale"; to: 0; duration: 1; }
+ PropertyAction { target: myDelegate; property: "ListView.delayRemove"; value: false }
+ }
+
+ }
+ footer: Rectangle {
+ id: listFooter
+ color: "blue"
+ width: 800
+ height: 100
+ }
+ }
+}
diff --git a/tests/auto/quick/qquicklistview2/data/highlightWithBound.qml b/tests/auto/quick/qquicklistview2/data/highlightWithBound.qml
new file mode 100644
index 0000000000..6cedd3e7d3
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/highlightWithBound.qml
@@ -0,0 +1,10 @@
+pragma ComponentBehavior: Bound
+import QtQuick
+
+ListView {
+ model: 3
+ delegate: Item {}
+ highlight: Item {
+ objectName: "highlight"
+ }
+}
diff --git a/tests/auto/quick/qquicklistview2/data/innerRequired.qml b/tests/auto/quick/qquicklistview2/data/innerRequired.qml
new file mode 100644
index 0000000000..c0862cec0d
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/innerRequired.qml
@@ -0,0 +1,35 @@
+import QtQuick
+
+Item {
+ ListModel {
+ id: myModel
+ ListElement { type: "Dog"; age: 8; noise: "meow" }
+ ListElement { type: "Cat"; age: 5; noise: "woof" }
+ }
+
+ component SomeDelegate: Item {
+ required property int age
+ property string text
+ }
+
+ component AnotherDelegate: Item {
+ property int age
+ property string text
+
+ SomeDelegate {
+ age: 0
+ text: ""
+ }
+ }
+
+ ListView {
+ id: listView
+ model: myModel
+ width: 100
+ height: 100
+ delegate: AnotherDelegate {
+ age: model.age
+ text: model.noise
+ }
+ }
+}
diff --git a/tests/auto/quick/qquicklistview2/data/maxXExtent.qml b/tests/auto/quick/qquicklistview2/data/maxXExtent.qml
new file mode 100644
index 0000000000..d72f825654
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/maxXExtent.qml
@@ -0,0 +1,29 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+
+Item {
+ property alias view: view
+
+ ListView {
+ id: view
+ model: 10
+ width: 200
+ height: 200
+
+ Rectangle {
+ anchors.fill: parent
+ color: "transparent"
+ border.color: "darkorange"
+ }
+
+ delegate: Rectangle {
+ width: 100
+ height: 100
+ Text {
+ text: modelData
+ }
+ }
+ }
+}
diff --git a/tests/auto/quick/qquicklistview2/data/maxYExtent.qml b/tests/auto/quick/qquicklistview2/data/maxYExtent.qml
new file mode 100644
index 0000000000..b8a1f0e12b
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/maxYExtent.qml
@@ -0,0 +1,30 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+
+Item {
+ property alias view: view
+
+ ListView {
+ id: view
+ model: 10
+ width: 200
+ height: 200
+ orientation: ListView.Horizontal
+
+ Rectangle {
+ anchors.fill: parent
+ color: "transparent"
+ border.color: "darkorange"
+ }
+
+ delegate: Rectangle {
+ width: 100
+ height: 100
+ Text {
+ text: modelData
+ }
+ }
+ }
+}
diff --git a/tests/auto/quick/qquicklistview2/data/metaSequenceAsModel.qml b/tests/auto/quick/qquicklistview2/data/metaSequenceAsModel.qml
new file mode 100644
index 0000000000..461450239f
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/metaSequenceAsModel.qml
@@ -0,0 +1,14 @@
+import QtQuick
+
+ListView {
+ id: view
+ width: 100
+ height: 100
+ property list<rect> rects: [ Qt.rect(1, 2, 3, 4), Qt.rect(5, 6, 7, 8) ]
+ property list<string> texts
+
+ model: rects
+ delegate: Item {
+ Component.onCompleted: view.texts.push(modelData.x + "/" + modelData.y)
+ }
+}
diff --git a/tests/auto/quick/qquicklistview2/data/mouseAreaDelegate.qml b/tests/auto/quick/qquicklistview2/data/mouseAreaDelegate.qml
new file mode 100644
index 0000000000..ad556913a5
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/mouseAreaDelegate.qml
@@ -0,0 +1,30 @@
+import QtQuick 2.15
+
+ListView {
+ id: root
+ width: 320
+ height: 480
+ model: 100
+
+ property var pressedDelegates: []
+ property var releasedDelegates: []
+ property var tappedDelegates: []
+ property var canceledDelegates: []
+
+ delegate: MouseArea {
+ height: 100
+ width: 320
+
+ onPressed: root.pressedDelegates.push(index)
+ onReleased: root.releasedDelegates.push(index)
+ onClicked: root.tappedDelegates.push(index)
+ onCanceled: root.canceledDelegates.push(index)
+
+ Rectangle {
+ id: buttonArea
+ anchors.fill: parent
+ border.color: "#41cd52"
+ color: parent.pressed ? "lightsteelblue" : "beige"
+ }
+ }
+}
diff --git a/tests/auto/quick/qquicklistview2/data/noCrashOnIndexChange.qml b/tests/auto/quick/qquicklistview2/data/noCrashOnIndexChange.qml
new file mode 100644
index 0000000000..6065d09981
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/noCrashOnIndexChange.qml
@@ -0,0 +1,48 @@
+import QtQuick
+import QtQml.Models
+
+Item {
+ ListModel {
+ id: myModel
+ ListElement { role_display: "One"; role_value: 0; }
+ ListElement { role_display: "One"; role_value: 2; }
+ ListElement { role_display: "One"; role_value: 3; }
+ ListElement { role_display: "One"; role_value: 4; }
+ ListElement { role_details: "Two"; role_value: 5; }
+ ListElement { role_details: "Three"; role_value: 6; }
+ ListElement { role_details: "Four"; role_value: 7; }
+ ListElement { role_details: "Five"; role_value: 8; }
+ ListElement { role_details: "Six"; role_value: 9; }
+ ListElement { role_keyID: "Seven"; role_value: 10; }
+ ListElement { role_keyID: "Eight"; role_value: 11; }
+ ListElement { role_keyID: "hello"; role_value: 12; }
+ }
+
+ DelegateModel {
+ id: displayDelegateModel
+ delegate: Text { text: role_display }
+ model: myModel
+ groups: [
+ DelegateModelGroup {
+ includeByDefault: false
+ name: "displayField"
+ }
+ ]
+ filterOnGroup: "displayField"
+ Component.onCompleted: {
+ var rowCount = myModel.count;
+ items.remove(0, rowCount);
+ for (var i = 0; i < rowCount; i++) {
+ var entry = myModel.get(i);
+ if (entry.role_display) {
+ items.insert(entry, "displayField");
+ }
+ }
+ }
+ }
+
+ ListView {
+ model: displayDelegateModel
+ }
+}
+
diff --git a/tests/auto/quick/qquicklistview2/data/qtbug104679_footer.qml b/tests/auto/quick/qquicklistview2/data/qtbug104679_footer.qml
new file mode 100644
index 0000000000..919cf4d2ec
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/qtbug104679_footer.qml
@@ -0,0 +1,21 @@
+import QtQuick
+
+Rectangle {
+ width: 640
+ height: 480
+
+ ListView {
+ anchors.fill: parent
+ spacing: 5
+
+ footerPositioning: ListView.PullBackFooter
+ footer: Rectangle { width: ListView.view.width; color: "blue"; implicitHeight: 46 }
+
+ model: 3 // crashed if less items than a full list page
+ delegate: Rectangle {
+ width: ListView.view.width
+ height: 50
+ color: index % 2 ? "black" : "gray"
+ }
+ }
+}
diff --git a/tests/auto/quick/qquicklistview2/data/qtbug104679_header.qml b/tests/auto/quick/qquicklistview2/data/qtbug104679_header.qml
new file mode 100644
index 0000000000..40ddf27988
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/qtbug104679_header.qml
@@ -0,0 +1,21 @@
+import QtQuick
+
+Rectangle {
+ width: 640
+ height: 480
+
+ ListView {
+ anchors.fill: parent
+ spacing: 5
+
+ headerPositioning: ListView.PullBackHeader
+ header: Rectangle { width: ListView.view.width; color: "red"; implicitHeight: 46 }
+
+ model: 3 // crashed if less items than a full list page
+ delegate: Rectangle {
+ width: ListView.view.width
+ height: 50
+ color: index % 2 ? "black" : "gray"
+ }
+ }
+}
diff --git a/tests/auto/quick/qquicklistview2/data/qtbug86744.qml b/tests/auto/quick/qquicklistview2/data/qtbug86744.qml
new file mode 100644
index 0000000000..d8b89a147d
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/qtbug86744.qml
@@ -0,0 +1,25 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+import QtQml.Models
+
+Item {
+ height: 200
+ width: 100
+ DelegateModel {
+ id: dm
+ model: 2
+ delegate: Item {
+ width: 100
+ height: 20
+ property bool isCurrent: ListView.isCurrentItem
+ }
+ }
+ ListView {
+ objectName: "listView"
+ model: dm
+ currentIndex: 1
+ anchors.fill: parent
+ }
+}
diff --git a/tests/auto/quick/qquicklistview2/data/qtbug98315.qml b/tests/auto/quick/qquicklistview2/data/qtbug98315.qml
new file mode 100644
index 0000000000..4035915c6d
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/qtbug98315.qml
@@ -0,0 +1,98 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+import QtQml.Models
+
+Item {
+ width: 500
+ height: 200
+
+ property list<QtObject> myModel: [
+ QtObject {
+ objectName: "Item 0"
+ property bool selected: true
+ },
+ QtObject {
+ objectName: "Item 1"
+ property bool selected: false
+ },
+ QtObject {
+ objectName: "Item 2"
+ property bool selected: false
+ },
+ QtObject {
+ objectName: "Item 3"
+ property bool selected: true
+ },
+ QtObject {
+ objectName: "Item 4"
+ property bool selected: true
+ },
+ QtObject {
+ objectName: "Item 5"
+ property bool selected: true
+ },
+ QtObject {
+ objectName: "Item 6"
+ property bool selected: false
+ }
+ ]
+
+ ListView {
+ objectName: "listView"
+ id: listview
+ width: 500
+ height: 200
+
+ focus: true
+ clip: true
+ spacing: 2
+ orientation: ListView.Horizontal
+ highlightMoveDuration: 300
+ highlightMoveVelocity: -1
+ preferredHighlightBegin: (500 - 100) / 2
+ preferredHighlightEnd: (500 + 100) / 2
+ highlightRangeMode: ListView.StrictlyEnforceRange
+ cacheBuffer: 500
+ currentIndex: 1
+
+ model: DelegateModel {
+ id: delegateModel
+ filterOnGroup: "visible"
+ model: myModel
+ groups: [
+ DelegateModelGroup {
+ name: "visible"
+ includeByDefault: true
+ }
+ ]
+ delegate: Rectangle {
+ id: tile
+ objectName: model.modelData.objectName
+
+ width: 100
+ height: 100
+ border.width: 0
+ anchors.verticalCenter: parent.verticalCenter
+
+ visible: model.modelData.selected
+ Component.onCompleted: {
+ DelegateModel.inPersistedItems = true
+ DelegateModel.inVisible = Qt.binding(function () {
+ return model.modelData.selected
+ })
+ }
+
+ property bool isCurrent: ListView.isCurrentItem
+ color: isCurrent ? "red" : "green"
+
+ Text {
+ id: valueText
+ anchors.centerIn: parent
+ text: model.modelData.objectName
+ }
+ }
+ }
+ }
+}
diff --git a/tests/auto/quick/qquicklistview2/data/qtbug_92809.qml b/tests/auto/quick/qquicklistview2/data/qtbug_92809.qml
new file mode 100644
index 0000000000..7507e83f73
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/qtbug_92809.qml
@@ -0,0 +1,71 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+
+Rectangle {
+ id: root
+ width: 800
+ height: 480
+
+ property list<QtObject> myModel: [
+ QtObject { property string name: "Item 0"; property bool selected: true },
+ QtObject { property string name: "Item 1"; property bool selected: true },
+ QtObject { property string name: "Item 2"; property bool selected: true },
+ QtObject { property string name: "Item 3"; property bool selected: true },
+ QtObject { property string name: "Item 4"; property bool selected: true },
+ QtObject { property string name: "Item 5"; property bool selected: true },
+ QtObject { property string name: "Item 6"; property bool selected: true },
+ QtObject { property string name: "Item 7"; property bool selected: true },
+ QtObject { property string name: "Item 8"; property bool selected: true },
+ QtObject { property string name: "Item 9"; property bool selected: true },
+ QtObject { property string name: "Press Enter here"; property bool selected: true }
+ ]
+
+ DelegateModel {
+ objectName: "model"
+ id: visualModel
+ model: myModel
+ filterOnGroup: "selected"
+
+ groups: [
+ DelegateModelGroup {
+ name: "selected"
+ includeByDefault: true
+ }
+ ]
+
+ delegate: Rectangle {
+ width: 180
+ height: 180
+ visible: DelegateModel.inSelected
+ color: ListView.isCurrentItem ? "orange" : "yellow"
+ Component.onCompleted: {
+ DelegateModel.inPersistedItems = true
+ DelegateModel.inSelected = Qt.binding(function() { return model.selected })
+ }
+ }
+ }
+
+ ListView {
+ objectName: "list"
+ anchors.fill: parent
+ spacing: 180/15
+ orientation: ListView.Horizontal
+ model: visualModel
+ focus: true
+ currentIndex: 0
+ preferredHighlightBegin: (width-180)/2
+ preferredHighlightEnd: (width+180)/2
+ highlightRangeMode: ListView.StrictlyEnforceRange
+ highlightMoveDuration: 300
+ highlightMoveVelocity: -1
+ cacheBuffer: 0
+
+ onCurrentIndexChanged: {
+ if (currentIndex === 10) {
+ myModel[6].selected = !myModel[6].selected
+ }
+ }
+ }
+}
diff --git a/tests/auto/quick/qquicklistview2/data/sectionBoundComponent.qml b/tests/auto/quick/qquicklistview2/data/sectionBoundComponent.qml
new file mode 100644
index 0000000000..74ab6b59fa
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/sectionBoundComponent.qml
@@ -0,0 +1,14 @@
+pragma ComponentBehavior: Bound
+import QtQuick
+ListView {
+ id: view
+ width: 100
+ height: 100
+ model: ListModel {
+ ListElement { name: "foo"; age: 42 }
+ ListElement { name: "bar"; age: 13 }
+ }
+ delegate: Text { required property string name; text: name}
+ section.property: "age"
+ section.delegate: Rectangle { color: "gray"; width: view.width; height: 20; required property string section; Text {text: parent.section} }
+}
diff --git a/tests/auto/quick/qquicklistview2/data/sectionGeometryChange.qml b/tests/auto/quick/qquicklistview2/data/sectionGeometryChange.qml
new file mode 100644
index 0000000000..6981af51ec
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/sectionGeometryChange.qml
@@ -0,0 +1,58 @@
+import QtQuick
+import QtQuick.Controls
+
+Rectangle {
+ width: 640
+ height: 480
+ color: "#FFFFFF"
+ ListView {
+ objectName: "list"
+ anchors.fill: parent
+
+ delegate: Rectangle {
+ objectName: value
+ implicitHeight: text.implicitHeight
+ color: "#ff3"
+
+ Text {
+ id: text
+ width: parent.width
+ padding: 5
+ font.pixelSize: 20
+ text: value
+ }
+ }
+
+ section {
+ property: "section"
+
+ delegate: Rectangle {
+ objectName: section
+ width: parent.width
+ implicitHeight: text.implicitHeight
+ color: "#3ff"
+
+ Text {
+ id: text
+ width: parent.width
+ padding: 5
+ font.pixelSize: 20
+ text: section
+ wrapMode: Text.Wrap
+ }
+ }
+ }
+
+ model: ListModel {
+ ListElement { value: "Element1"; section: "Section1" }
+ ListElement { value: "Element2"; section: "Section1" }
+ ListElement { value: "Element3"; section: "Section1" }
+ ListElement { value: "Element4"; section: "Section2" }
+ ListElement { value: "Element5"; section: "Section2" }
+ ListElement { value: "Element6"; section: "Section2" }
+ ListElement { value: "Element7"; section: "Section2" }
+ ListElement { value: "Element8"; section: "Section3" }
+ ListElement { value: "Element9"; section: "Section3" }
+ }
+ }
+}
diff --git a/tests/auto/quick/qquicklistview2/data/sectionsNoOverlap.qml b/tests/auto/quick/qquicklistview2/data/sectionsNoOverlap.qml
new file mode 100644
index 0000000000..3a22626032
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/sectionsNoOverlap.qml
@@ -0,0 +1,77 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+import QtQuick.Controls
+
+Rectangle {
+ property string sectionProperty: "section"
+ property int sectionPositioning: ViewSection.InlineLabels
+
+ width: 640
+ height: 480
+ color: "#FFFFFF"
+
+ resources: [
+ Component {
+ id: myDelegate
+ Text {
+ objectName: model.title
+ width: parent.width
+ height: 40
+ text: "NormalDelegate: " + model.title
+ visible: model.isVisible
+ verticalAlignment: Text.AlignVCenter
+ }
+ }
+ ]
+ ListView {
+ id: list
+ objectName: "list"
+ anchors.fill: parent
+ clip: true
+
+ model: ListModel {
+ ListElement {
+ title: "element1"
+ isVisible: true
+ section: "section1"
+ }
+ ListElement {
+ title: "element2"
+ isVisible: true
+ section: "section1"
+ }
+ ListElement {
+ title: "element3"
+ isVisible: true
+ section: "section2"
+ }
+ ListElement {
+ title: "element4"
+ isVisible: true
+ section: "section2"
+ }
+ }
+
+ delegate: myDelegate
+
+ section.property: "section"
+ section.criteria: ViewSection.FullString
+ section.delegate: Component {
+ Text {
+ id: sectionDelegate
+ objectName: section
+ visible: false
+ width: parent.width
+ height: visible ? 48 : 0
+ text: "Section delegate: " + section
+ verticalAlignment: Text.AlignVCenter
+ elide: Text.ElideMiddle
+ Component.onCompleted: function(){
+ Qt.callLater(function(){sectionDelegate.visible = true})
+ }
+ }
+ }
+ }
+}
diff --git a/tests/auto/quick/qquicklistview2/data/singletonModelLifetime.qml b/tests/auto/quick/qquicklistview2/data/singletonModelLifetime.qml
new file mode 100644
index 0000000000..f230786723
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/singletonModelLifetime.qml
@@ -0,0 +1,32 @@
+import QtQuick 2.15
+import QtQuick.Window 2.15
+import test 1.0
+
+Window {
+ id: root
+ visible: true
+ width: 800
+ height: 680
+ property bool alive: false
+
+ Component {
+ id: view
+ ListView {
+ model: SingletonModel
+ }
+ }
+ function compare(a,b) {
+ root.alive = (a === b)
+ }
+
+ function test_singletonModelCrash() {
+ SingletonModel.objectName = "model"
+ var o = view.createObject(root)
+ o.destroy()
+ Qt.callLater(function() {
+ compare(SingletonModel.objectName, "model")
+ })
+ }
+
+ Component.onCompleted: root.test_singletonModelCrash()
+}
diff --git a/tests/auto/quick/qquicklistview2/data/snapOneItem.qml b/tests/auto/quick/qquicklistview2/data/snapOneItem.qml
new file mode 100644
index 0000000000..a27f220865
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/snapOneItem.qml
@@ -0,0 +1,34 @@
+import QtQuick
+
+ListView {
+ id: list
+ snapMode: ListView.SnapOneItem
+ model: 4
+ width: 200
+ height: 200
+ highlightRangeMode: ListView.StrictlyEnforceRange
+ highlight: Rectangle { width: 200; height: 200; color: "yellow" }
+ delegate: Rectangle {
+ id: wrapper
+ width: list.width
+ height: list.height
+ Column {
+ Text {
+ text: index
+ }
+ Text {
+ text: wrapper.x + ", " + wrapper.y
+ }
+ }
+ color: ListView.isCurrentItem ? "lightsteelblue" : "transparent"
+ }
+ // speed up test runs
+ flickDeceleration: 5000
+ rebound: Transition {
+ NumberAnimation {
+ properties: "x,y"
+ duration: 30
+ easing.type: Easing.OutBounce
+ }
+ }
+}
diff --git a/tests/auto/quick/qquicklistview2/data/urlListModel.qml b/tests/auto/quick/qquicklistview2/data/urlListModel.qml
new file mode 100644
index 0000000000..38237234e0
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/urlListModel.qml
@@ -0,0 +1,22 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+
+Rectangle {
+ id: root
+
+ property var model
+ property alias view: view
+
+ ListView {
+ id: view
+ anchors.fill: parent
+ objectName: "view"
+ model: root.model
+ delegate: Text {
+ height: view.width
+ text: modelData
+ }
+ }
+}
diff --git a/tests/auto/quick/qquicklistview2/data/viewportAvoidUndesiredMovementOnSetCurrentIndex.qml b/tests/auto/quick/qquicklistview2/data/viewportAvoidUndesiredMovementOnSetCurrentIndex.qml
new file mode 100644
index 0000000000..cd3865d55b
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/data/viewportAvoidUndesiredMovementOnSetCurrentIndex.qml
@@ -0,0 +1,47 @@
+import QtQuick
+
+Item {
+ id: root
+ width: 400
+ height: 600
+
+ ListView {
+ id: rawList
+ objectName: "list"
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ height: 300
+
+ // full disabling of automatic viewport positioning
+ highlightFollowsCurrentItem: false
+ snapMode: ListView.NoSnap
+ highlightRangeMode: ListView.NoHighlightRange
+
+ delegate: Rectangle {
+ color: model.index === rawList.currentIndex ? "red" : "white"
+ border.color: rawList.currentItem === this ? "red" : "black"
+ height: 100
+ width: 400
+
+ Text {
+ anchors.centerIn: parent
+ text: model.index
+ font.pixelSize: 50
+ }
+
+ MouseArea {
+ // only for using this file to do manual testing
+ // autotest calls setCurrentIndex
+ anchors.fill: parent
+
+ onClicked: {
+ rawList.currentIndex = model.index;
+ }
+ }
+ }
+
+ model: 30
+ }
+
+}
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 = [&timestamp](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"
diff --git a/tests/auto/quick/qquicklistview2/typerolemodel.cpp b/tests/auto/quick/qquicklistview2/typerolemodel.cpp
new file mode 100644
index 0000000000..94da87beda
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/typerolemodel.cpp
@@ -0,0 +1,41 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include "typerolemodel.h"
+
+TypeRoleModel::TypeRoleModel(QObject *parent)
+ : QAbstractListModel(parent)
+{
+ _mapRoleNames[TypeRole] = "type";
+ _mapRoleNames[TextRole] = "text";
+}
+
+int TypeRoleModel::rowCount(const QModelIndex &) const
+{
+ return 3;
+}
+
+QVariant TypeRoleModel::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid())
+ return {};
+
+ constexpr Type types[] = {
+ Type::PlainText,
+ Type::Markdown,
+ Type::Rect
+ };
+ switch (role) {
+ case TypeRole: {
+ const Type type = types[index.row() % std::size(types)];
+ return QVariant::fromValue(type);
+ }
+ case TextRole: {
+ if (index.row() % std::size(types) == int(Type::Markdown))
+ return "*row* " + QString::number(index.row());
+ return "row " + QString::number(index.row());
+ }
+ }
+
+ return {};
+}
diff --git a/tests/auto/quick/qquicklistview2/typerolemodel.h b/tests/auto/quick/qquicklistview2/typerolemodel.h
new file mode 100644
index 0000000000..f47400a88c
--- /dev/null
+++ b/tests/auto/quick/qquicklistview2/typerolemodel.h
@@ -0,0 +1,30 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include <QAbstractListModel>
+#include <qqml.h>
+
+class TypeRoleModel : public QAbstractListModel
+{
+ Q_OBJECT
+ QML_ELEMENT
+
+public:
+ enum Role {
+ TypeRole = Qt::UserRole + 1,
+ TextRole,
+ };
+ Q_ENUM(Role)
+
+ enum class Type { PlainText, Markdown, Rect };
+ Q_ENUM(Type)
+
+ explicit TypeRoleModel(QObject *parent = nullptr);
+
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+ QHash<int, QByteArray> roleNames() const override { return _mapRoleNames; }
+
+private:
+ QHash<int, QByteArray> _mapRoleNames;
+};