blob: 5b3ef6e30f72f63ee81b17e17f2a4840b4a0bdaa [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the test suite of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:BSD$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** BSD License Usage
** Alternatively, you may use this file under the terms of the BSD license
** as follows:
**
** "Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
** * Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** * Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in
** the documentation and/or other materials provided with the
** distribution.
** * Neither the name of The Qt Company Ltd nor the names of its
** contributors may be used to endorse or promote products derived
** from this software without specific prior written permission.
**
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
**
** $QT_END_LICENSE$
**
****************************************************************************/
import QtQuick 2.12
import QtTest 1.0
import QtQuick.Controls 2.12
TestCase {
id: testCase
width: 300
height: 300
visible: true
when: windowShown
name: "Tumbler"
property var tumbler: null
readonly property real implicitTumblerWidth: 60
readonly property real implicitTumblerHeight: 200
readonly property real defaultImplicitDelegateHeight: implicitTumblerHeight / 3
readonly property real defaultListViewTumblerOffset: -defaultImplicitDelegateHeight
readonly property real tumblerDelegateHeight: tumbler ? tumbler.availableHeight / tumbler.visibleItemCount : 0
property Item tumblerView: null
Component {
id: tumblerComponent
Tumbler {
visibleItemCount: 3
}
}
Component {
id: itemComponent
Item {
anchors.fill: parent
}
}
function createTumbler(args) {
tumbler = createTemporaryObject(tumblerComponent, testCase, args);
verify(tumbler, "Tumbler: failed to create an instance");
tumblerView = findView(tumbler);
verify(tumblerView);
}
function tumblerXCenter() {
return tumbler.leftPadding + tumbler.width / 2;
}
function tumblerYCenter() {
return tumbler.topPadding + tumbler.height / 2;
}
// visualItemIndex is from 0 to the amount of visible items.
function itemCenterPos(visualItemIndex) {
var halfDelegateHeight = tumblerDelegateHeight / 2;
var yCenter = tumbler.y + tumbler.topPadding + halfDelegateHeight
+ (tumblerDelegateHeight * visualItemIndex);
return Qt.point(tumblerXCenter(), yCenter);
}
function itemTopLeftPos(visualItemIndex) {
return Qt.point(tumbler.leftPadding, tumbler.topPadding + (tumblerDelegateHeight * visualItemIndex));
}
function checkItemSizes() {
var contentChildren = tumbler.wrap ? tumblerView.children : tumblerView.contentItem.children;
verify(contentChildren.length >= tumbler.count);
for (var i = 0; i < contentChildren.length; ++i) {
compare(contentChildren[i].width, tumbler.availableWidth);
compare(contentChildren[i].height, tumblerDelegateHeight);
}
}
function findView(parent) {
for (var i = 0; i < parent.children.length; ++i) {
var child = parent.children[i];
if (child.hasOwnProperty("currentIndex")) {
return child;
}
var grandChild = findView(child);
if (grandChild)
return grandChild;
}
return null;
}
function findDelegateWithText(parent, text) {
for (var i = 0; i < parent.children.length; ++i) {
var child = parent.children[i];
if (child.hasOwnProperty("text") && child.text === text) {
return child;
}
var grandChild = findDelegateWithText(child, text);
if (grandChild)
return grandChild;
}
return null;
}
property Component noAttachedPropertiesDelegate: Text {
text: modelData
}
function test_wrapWithoutAttachedProperties() {
createTumbler();
verify(tumbler.wrap);
tumbler.delegate = noAttachedPropertiesDelegate;
// Shouldn't assert.
tumbler.wrap = false;
verify(findView(tumbler));
}
// TODO: test that currentIndex is maintained between contentItem changes...
// function tst_dynamicContentItemChange() {
// }
function test_currentIndex() {
createTumbler();
compare(tumbler.contentItem.parent, tumbler);
tumbler.model = 5;
compare(tumbler.currentIndex, 0);
waitForRendering(tumbler);
// Set it through user interaction.
var pos = Qt.point(tumblerXCenter(), tumbler.height / 2);
mouseDrag(tumbler, pos.x, pos.y, 0, tumbler.height / 3, Qt.LeftButton, Qt.NoModifier, 200);
tryCompare(tumblerView, "offset", 1);
compare(tumbler.currentIndex, 4);
compare(tumblerView.currentIndex, 4);
// Set it manually.
tumbler.currentIndex = 2;
tryCompare(tumbler, "currentIndex", 2);
compare(tumblerView.currentIndex, 2);
tumbler.model = null;
tryCompare(tumbler, "currentIndex", -1);
// PathView will only use 0 as the currentIndex when there are no items.
compare(tumblerView.currentIndex, 0);
tumbler.model = ["A", "B", "C"];
tryCompare(tumbler, "currentIndex", 0);
// Setting a negative current index should have no effect, because the model isn't empty.
tumbler.currentIndex = -1;
compare(tumbler.currentIndex, 0);
tumbler.model = 1;
compare(tumbler.currentIndex, 0);
tumbler.model = 5;
compare(tumbler.count, 5);
tumblerView = findView(tumbler);
tryCompare(tumblerView, "count", 5);
tumbler.currentIndex = 4;
compare(tumbler.currentIndex, 4);
compare(tumblerView.currentIndex, 4);
--tumbler.model;
compare(tumbler.count, 4);
compare(tumblerView.count, 4);
// Removing an item from an integer-based model will cause views to reset their currentIndex to 0.
compare(tumbler.currentIndex, 0);
compare(tumblerView.currentIndex, 0);
tumbler.model = 0;
compare(tumbler.currentIndex, -1);
}
Component {
id: currentIndexTumbler
Tumbler {
model: 5
currentIndex: 2
visibleItemCount: 3
}
}
Component {
id: currentIndexTumblerNoWrap
Tumbler {
model: 5
currentIndex: 2
wrap: false
visibleItemCount: 3
}
}
Component {
id: currentIndexTumblerNoWrapReversedOrder
Tumbler {
model: 5
wrap: false
currentIndex: 2
visibleItemCount: 3
}
}
Component {
id: negativeCurrentIndexTumblerNoWrap
Tumbler {
model: 5
wrap: false
currentIndex: -1
visibleItemCount: 3
}
}
Component {
id: currentIndexTooLargeTumbler
Tumbler {
objectName: "currentIndexTooLargeTumbler"
model: 10
currentIndex: 10
}
}
function test_currentIndexAtCreation_data() {
return [
{ tag: "wrap: implicit, expected currentIndex: 2", currentIndex: 2, wrap: true, component: currentIndexTumbler },
{ tag: "wrap: false, expected currentIndex: 2", currentIndex: 2, wrap: false, component: currentIndexTumblerNoWrap },
// Order of property assignments shouldn't matter
{ tag: "wrap: false, expected currentIndex: 2, reversed property assignment order",
currentIndex: 2, wrap: false, component: currentIndexTumblerNoWrapReversedOrder },
{ tag: "wrap: false, expected currentIndex: 0", currentIndex: 0, wrap: false, component: negativeCurrentIndexTumblerNoWrap },
{ tag: "wrap: implicit, expected currentIndex: 0", currentIndex: 0, wrap: true, component: currentIndexTooLargeTumbler }
]
}
function test_currentIndexAtCreation(data) {
// Test setting currentIndex at creation time
tumbler = createTemporaryObject(data.component, testCase);
verify(tumbler);
// A "statically declared" currentIndex will be pending until the count has changed,
// which happens when the model is set, which happens on the TumblerView's next polish.
tryCompare(tumbler, "currentIndex", data.currentIndex);
tumblerView = findView(tumbler);
tryVerify(function() { return tumblerView.currentItem });
compare(tumblerView.currentIndex, data.currentIndex);
compare(tumblerView.currentItem.text, data.currentIndex.toString());
if (data.wrap) {
tryCompare(tumblerView, "offset", data.currentIndex > 0 ? tumblerView.count - data.currentIndex : 0);
} else {
tryCompare(tumblerView, "contentY", tumblerDelegateHeight * data.currentIndex - tumblerView.preferredHighlightBegin);
}
}
function test_keyboardNavigation() {
createTumbler();
tumbler.model = 5;
tumbler.forceActiveFocus();
tumblerView.highlightMoveDuration = 0;
// Navigate upwards through entire wheel.
for (var j = 0; j < tumbler.count - 1; ++j) {
keyClick(Qt.Key_Up, Qt.NoModifier);
tryCompare(tumblerView, "offset", j + 1);
compare(tumbler.currentIndex, tumbler.count - 1 - j);
}
keyClick(Qt.Key_Up, Qt.NoModifier);
tryCompare(tumblerView, "offset", 0);
compare(tumbler.currentIndex, 0);
// Navigate downwards through entire wheel.
for (j = 0; j < tumbler.count - 1; ++j) {
keyClick(Qt.Key_Down, Qt.NoModifier);
tryCompare(tumblerView, "offset", tumbler.count - 1 - j);
compare(tumbler.currentIndex, j + 1);
}
keyClick(Qt.Key_Down, Qt.NoModifier);
tryCompare(tumblerView, "offset", 0);
compare(tumbler.currentIndex, 0);
}
function test_itemsCorrectlyPositioned() {
createTumbler();
tumbler.model = 4;
tumbler.height = 120;
compare(tumblerDelegateHeight, 40);
checkItemSizes();
wait(tumblerView.highlightMoveDuration);
var firstItemCenterPos = itemCenterPos(1);
var firstItem = tumblerView.itemAt(firstItemCenterPos.x, firstItemCenterPos.y);
var actualPos = testCase.mapFromItem(firstItem, 0, 0);
compare(actualPos.x, tumbler.leftPadding);
compare(actualPos.y, tumbler.topPadding + 40);
tumbler.forceActiveFocus();
keyClick(Qt.Key_Down);
tryCompare(tumblerView, "offset", 3.0);
tryCompare(tumbler, "moving", false);
firstItemCenterPos = itemCenterPos(0);
firstItem = tumblerView.itemAt(firstItemCenterPos.x, firstItemCenterPos.y);
verify(firstItem);
// Test QTBUG-40298.
actualPos = testCase.mapFromItem(firstItem, 0, 0);
fuzzyCompare(actualPos.x, tumbler.leftPadding, 0.0001);
fuzzyCompare(actualPos.y, tumbler.topPadding, 0.0001);
var secondItemCenterPos = itemCenterPos(1);
var secondItem = tumblerView.itemAt(secondItemCenterPos.x, secondItemCenterPos.y);
verify(secondItem);
verify(firstItem.y < secondItem.y);
var thirdItemCenterPos = itemCenterPos(2);
var thirdItem = tumblerView.itemAt(thirdItemCenterPos.x, thirdItemCenterPos.y);
verify(thirdItem);
verify(firstItem.y < thirdItem.y);
verify(secondItem.y < thirdItem.y);
}
function test_focusPastTumbler() {
tumbler = createTemporaryObject(tumblerComponent, testCase);
verify(tumbler);
var mouseArea = createTemporaryQmlObject(
"import QtQuick 2.2; TextInput { activeFocusOnTab: true; width: 50; height: 50 }", testCase, "");
tumbler.forceActiveFocus();
verify(tumbler.activeFocus);
keyClick(Qt.Key_Tab);
verify(!tumbler.activeFocus);
verify(mouseArea.activeFocus);
}
function test_datePicker() {
var component = Qt.createComponent("TumblerDatePicker.qml");
compare(component.status, Component.Ready, component.errorString());
tumbler = createTemporaryObject(component, testCase);
// Should not be any warnings.
tryCompare(tumbler.dayTumbler, "currentIndex", 0);
compare(tumbler.dayTumbler.count, 31);
compare(tumbler.monthTumbler.currentIndex, 0);
compare(tumbler.monthTumbler.count, 12);
compare(tumbler.yearTumbler.currentIndex, 0);
tryCompare(tumbler.yearTumbler, "count", 100);
verify(findView(tumbler.dayTumbler).children.length >= tumbler.dayTumbler.visibleItemCount);
verify(findView(tumbler.monthTumbler).children.length >= tumbler.monthTumbler.visibleItemCount);
// TODO: do this properly somehow
wait(100);
verify(findView(tumbler.yearTumbler).children.length >= tumbler.yearTumbler.visibleItemCount);
// March.
tumbler.monthTumbler.currentIndex = 2;
tryCompare(tumbler.monthTumbler, "currentIndex", 2);
// 30th of March.
tumbler.dayTumbler.currentIndex = 29;
tryCompare(tumbler.dayTumbler, "currentIndex", 29);
// February.
tumbler.monthTumbler.currentIndex = 1;
tryCompare(tumbler.monthTumbler, "currentIndex", 1);
tryCompare(tumbler.dayTumbler, "currentIndex", 27);
}
Component {
id: timePickerComponent
Row {
property alias minuteTumbler: minuteTumbler
property alias amPmTumbler: amPmTumbler
Tumbler {
id: minuteTumbler
currentIndex: 6
model: 60
width: 50
height: 150
}
Tumbler {
id: amPmTumbler
model: ["AM", "PM"]
width: 50
height: 150
contentItem: ListView {
anchors.fill: parent
model: amPmTumbler.model
delegate: amPmTumbler.delegate
}
}
}
}
function test_listViewTimePicker() {
var root = createTemporaryObject(timePickerComponent, testCase);
verify(root);
mouseDrag(root.minuteTumbler, root.minuteTumbler.width / 2, root.minuteTumbler.height / 2, 0, 50);
// Shouldn't crash.
mouseDrag(root.amPmTumbler, root.amPmTumbler.width / 2, root.amPmTumbler.height / 2, 0, 50);
}
function test_displacement_data() {
var data = [
// At 0 offset, the first item is current.
{ count: 6, index: 0, offset: 0, expectedDisplacement: 0 },
{ count: 6, index: 1, offset: 0, expectedDisplacement: -1 },
{ count: 6, index: 5, offset: 0, expectedDisplacement: 1 },
// When we start to move the first item down, the second item above it starts to become current.
{ count: 6, index: 0, offset: 0.25, expectedDisplacement: -0.25 },
{ count: 6, index: 1, offset: 0.25, expectedDisplacement: -1.25 },
{ count: 6, index: 5, offset: 0.25, expectedDisplacement: 0.75 },
{ count: 6, index: 0, offset: 0.5, expectedDisplacement: -0.5 },
{ count: 6, index: 1, offset: 0.5, expectedDisplacement: -1.5 },
{ count: 6, index: 5, offset: 0.5, expectedDisplacement: 0.5 },
// By this stage, the delegate at index 1 is destroyed, so we can't test its displacement.
{ count: 6, index: 0, offset: 0.75, expectedDisplacement: -0.75 },
{ count: 6, index: 5, offset: 0.75, expectedDisplacement: 0.25 },
{ count: 6, index: 0, offset: 4.75, expectedDisplacement: 1.25 },
{ count: 6, index: 1, offset: 4.75, expectedDisplacement: 0.25 },
{ count: 6, index: 0, offset: 4.5, expectedDisplacement: 1.5 },
{ count: 6, index: 1, offset: 4.5, expectedDisplacement: 0.5 },
{ count: 6, index: 0, offset: 4.25, expectedDisplacement: 1.75 },
{ count: 6, index: 1, offset: 4.25, expectedDisplacement: 0.75 },
// count == visibleItemCount
{ count: 3, index: 0, offset: 0, expectedDisplacement: 0 },
{ count: 3, index: 1, offset: 0, expectedDisplacement: -1 },
{ count: 3, index: 2, offset: 0, expectedDisplacement: 1 },
// count < visibleItemCount
{ count: 2, index: 0, offset: 0, expectedDisplacement: 0 },
{ count: 2, index: 1, offset: 0, expectedDisplacement: 1 },
// count == 1
{ count: 1, index: 0, offset: 0, expectedDisplacement: 0 }
];
for (var i = 0; i < data.length; ++i) {
var row = data[i];
row.tag = "delegate" + row.index + " offset=" + row.offset + " expectedDisplacement=" + row.expectedDisplacement;
}
return data;
}
property Component displacementDelegate: Text {
objectName: "delegate" + index
text: modelData
opacity: 0.2 + Math.max(0, 1 - Math.abs(Tumbler.displacement)) * 0.8
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
Text {
text: parent.displacement.toFixed(2)
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
property real displacement: Tumbler.displacement
}
function test_displacement(data) {
createTumbler();
// TODO: test setting these in the opposite order (delegate after model
// doesn't seem to cause a change in delegates in PathView)
tumbler.wrap = true;
tumbler.delegate = displacementDelegate;
tumbler.model = data.count;
compare(tumbler.count, data.count);
var delegate = findChild(tumblerView, "delegate" + data.index);
verify(delegate);
tumblerView.offset = data.offset;
compare(delegate.displacement, data.expectedDisplacement);
// test displacement after adding and removing items
}
function test_wrap() {
createTumbler();
tumbler.model = 5;
compare(tumbler.count, 5);
tumbler.currentIndex = 2;
compare(tumblerView.currentIndex, 2);
tumbler.wrap = false;
tumblerView = findView(tumbler);
compare(tumbler.count, 5);
compare(tumbler.currentIndex, 2);
// Tumbler's count hasn't changed (the model hasn't changed),
// but the new view needs time to instantiate its items.
tryCompare(tumblerView, "count", 5);
compare(tumblerView.currentIndex, 2);
}
Component {
id: twoItemTumbler
Tumbler {
model: 2
}
}
Component {
id: tenItemTumbler
Tumbler {
model: 10
}
}
function test_countWrap() {
tumbler = createTemporaryObject(tumblerComponent, testCase);
verify(tumbler);
// Check that a count that is less than visibleItemCount results in wrap being set to false.
verify(2 < tumbler.visibleItemCount);
tumbler.model = 2;
compare(tumbler.count, 2);
compare(tumbler.wrap, false);
}
function test_explicitlyNonwrapping() {
// Check that explicitly setting wrap to false works even when it was implicitly false.
var explicitlyNonWrapping = createTemporaryObject(twoItemTumbler, testCase);
verify(explicitlyNonWrapping);
tryCompare(explicitlyNonWrapping, "wrap", false);
explicitlyNonWrapping.wrap = false;
// wrap shouldn't be set to true now that there are more items than there are visible ones.
verify(10 > explicitlyNonWrapping.visibleItemCount);
explicitlyNonWrapping.model = 10;
compare(explicitlyNonWrapping.wrap, false);
// Test resetting wrap back to the default behavior.
explicitlyNonWrapping.wrap = undefined;
compare(explicitlyNonWrapping.wrap, true);
}
function test_explicitlyWrapping() {
// Check that explicitly setting wrap to true works even when it was implicitly true.
var explicitlyWrapping = createTemporaryObject(tenItemTumbler, testCase);
verify(explicitlyWrapping);
compare(explicitlyWrapping.wrap, true);
explicitlyWrapping.wrap = true;
// wrap shouldn't be set to false now that there are more items than there are visible ones.
explicitlyWrapping.model = 2;
compare(explicitlyWrapping.wrap, true);
// Test resetting wrap back to the default behavior.
explicitlyWrapping.wrap = undefined;
compare(explicitlyWrapping.wrap, false);
}
Component {
id: customListViewTumblerComponent
Tumbler {
id: listViewTumbler
contentItem: ListView {
anchors.fill: parent
model: listViewTumbler.model
delegate: listViewTumbler.delegate
snapMode: ListView.SnapToItem
highlightRangeMode: ListView.StrictlyEnforceRange
preferredHighlightBegin: height / 2 - (height / listViewTumbler.visibleItemCount / 2)
preferredHighlightEnd: height / 2 + (height / listViewTumbler.visibleItemCount / 2)
clip: true
}
}
}
Component {
id: customPathViewTumblerComponent
Tumbler {
id: pathViewTumbler
contentItem: PathView {
id: pathView
model: pathViewTumbler.model
delegate: pathViewTumbler.delegate
clip: true
pathItemCount: pathViewTumbler.visibleItemCount + 1
preferredHighlightBegin: 0.5
preferredHighlightEnd: 0.5
dragMargin: width / 2
path: Path {
startX: pathView.width / 2
startY: -pathView.delegateHeight / 2
PathLine {
x: pathView.width / 2
y: pathView.pathItemCount * pathView.delegateHeight - pathView.delegateHeight / 2
}
}
property real delegateHeight: pathViewTumbler.availableHeight / pathViewTumbler.visibleItemCount
}
}
}
function test_customContentItemAtConstruction_data() {
return [
{ tag: "ListView", component: customListViewTumblerComponent },
{ tag: "PathView", component: customPathViewTumblerComponent }
];
}
function test_customContentItemAtConstruction(data) {
var tumbler = createTemporaryObject(data.component, testCase);
// Shouldn't assert.
tumbler.model = 5;
compare(tumbler.count, 5);
tumbler.currentIndex = 2;
var tumblerView = findView(tumbler);
compare(tumblerView.currentIndex, 2);
tumblerView.incrementCurrentIndex();
compare(tumblerView.currentIndex, 3);
compare(tumbler.currentIndex, 3);
// Shouldn't have any affect.
tumbler.wrap = false;
compare(tumbler.count, 5);
compare(tumblerView.currentIndex, 3);
compare(tumbler.currentIndex, 3);
}
function findFirstDelegateWithText(view, text) {
var delegate = null;
var contentItem = view.hasOwnProperty("contentItem") ? view.contentItem : view;
for (var i = 0; i < contentItem.children.length && !delegate; ++i) {
var child = contentItem.children[i];
if (child.hasOwnProperty("text") && child.text === text)
delegate = child;
}
return delegate;
}
function test_customContentItemAfterConstruction_data() {
return [
{ tag: "ListView", componentPath: "TumblerListView.qml" },
{ tag: "PathView", componentPath: "TumblerPathView.qml" }
];
}
function test_customContentItemAfterConstruction(data) {
createTumbler();
tumbler.model = 5;
compare(tumbler.count, 5);
tumbler.currentIndex = 2;
compare(tumblerView.currentIndex, 2);
var contentItemComponent = Qt.createComponent(data.componentPath);
compare(contentItemComponent.status, Component.Ready);
var customContentItem = createTemporaryObject(contentItemComponent, tumbler);
tumbler.contentItem = customContentItem;
compare(tumbler.count, 5);
tumblerView = findView(tumbler);
compare(tumblerView.currentIndex, 2);
var delegate = findFirstDelegateWithText(tumblerView, "Custom2");
verify(delegate);
compare(delegate.height, defaultImplicitDelegateHeight);
tryCompare(delegate.Tumbler, "displacement", 0);
tumblerView.incrementCurrentIndex();
compare(tumblerView.currentIndex, 3);
compare(tumbler.currentIndex, 3);
}
function test_displacementListView_data() {
var offset = defaultListViewTumblerOffset;
var data = [
// At 0 contentY, the first item is current.
{ contentY: offset, expectedDisplacements: [
{ index: 0, displacement: 0 },
{ index: 1, displacement: -1 },
{ index: 2, displacement: -2 } ]
},
// When we start to move the first item down, the second item above it starts to become current.
{ contentY: offset + defaultImplicitDelegateHeight * 0.25, expectedDisplacements: [
{ index: 0, displacement: 0.25 },
{ index: 1, displacement: -0.75 },
{ index: 2, displacement: -1.75 } ]
},
{ contentY: offset + defaultImplicitDelegateHeight * 0.5, expectedDisplacements: [
{ index: 0, displacement: 0.5 },
{ index: 1, displacement: -0.5 },
{ index: 2, displacement: -1.5 } ]
},
{ contentY: offset + defaultImplicitDelegateHeight * 0.75, expectedDisplacements: [
{ index: 0, displacement: 0.75 },
{ index: 1, displacement: -0.25 } ]
},
{ contentY: offset + defaultImplicitDelegateHeight * 3.5, expectedDisplacements: [
{ index: 3, displacement: 0.5 },
{ index: 4, displacement: -0.5 } ]
}
];
for (var i = 0; i < data.length; ++i) {
var row = data[i];
row.tag = "contentY=" + row.contentY;
}
return data;
}
function test_displacementListView(data) {
createTumbler();
tumbler.wrap = false;
tumbler.delegate = displacementDelegate;
tumbler.model = 5;
compare(tumbler.count, 5);
// Ensure assumptions about the tumbler used in our data() function are correct.
tumblerView = findView(tumbler);
compare(tumblerView.contentY, -defaultImplicitDelegateHeight);
var delegateCount = 0;
var listView = tumblerView;
var listViewContentItem = tumblerView.contentItem;
// We use the mouse instead of setting contentY directly, otherwise the
// items snap back into place. This doesn't seem to be an issue for
// PathView for some reason.
//
// I tried lots of things to get this test to work with small changes
// in ListView's contentY (to match the tests for a PathView-based Tumbler), but they didn't work:
//
// - Pressing once and then directly moving the mouse to the correct location
// - Pressing once and interpolating the mouse position to the correct location
// - Pressing once and doing some dragging up and down to trigger the
// overThreshold of QQuickFlickable
//
// Even after the last item above, QQuickFlickable wouldn't consider it a drag.
// It seems that overThreshold is set too late, and because the drag distance is quite small
// to begin with, nothing changes (the displacement was always very close to 0 in the end).
// Ensure that we at least cover the distance required to reach the desired contentY.
var distanceToReachContentY = data.contentY - defaultListViewTumblerOffset;
var distance = Math.abs(distanceToReachContentY) + tumbler.height / 2;
// If distanceToReachContentY is 0, we're testing 0 displacement, so we don't need to do anything.
if (distanceToReachContentY != 0) {
mousePress(tumbler, tumblerXCenter(), tumblerYCenter());
var dragDirection = distanceToReachContentY > 0 ? -1 : 1;
for (var i = 0; i < distance && Math.floor(listView.contentY) !== Math.floor(data.contentY); ++i) {
mouseMove(tumbler, tumblerXCenter(), tumblerYCenter() + i * dragDirection);
wait(1); // because Flickable pays attention to velocity, we need some time between movements (qtdeclarative ebf07c3)
}
}
for (var i = 0; i < data.expectedDisplacements.length; ++i) {
var delegate = findChild(listViewContentItem, "delegate" + data.expectedDisplacements[i].index);
verify(delegate);
compare(delegate.height, defaultImplicitDelegateHeight);
// Due to the way we must perform this test, we can't expect high precision.
var expectedDisplacement = data.expectedDisplacements[i].displacement;
fuzzyCompare(delegate.displacement, expectedDisplacement, 0.1,
"Delegate of ListView-based Tumbler at index " + data.expectedDisplacements[i].index
+ " has displacement of " + delegate.displacement + " when it should be " + expectedDisplacement);
}
if (distanceToReachContentY != 0)
mouseRelease(tumbler, tumblerXCenter(), itemCenterPos(1) + (data.contentY - defaultListViewTumblerOffset), Qt.LeftButton);
}
function test_listViewFlickAboveBounds_data() {
// Tests that flicking above the bounds when already at the top of the
// tumbler doesn't result in an incorrect displacement.
var data = [];
// Less than two items doesn't make sense. The default visibleItemCount
// is 3, so we test a bit more than double that.
for (var i = 2; i <= 7; ++i) {
data.push({ tag: i + " items", model: i });
}
return data;
}
function test_listViewFlickAboveBounds(data) {
createTumbler();
tumbler.wrap = false;
tumbler.delegate = displacementDelegate;
tumbler.model = data.model;
tumblerView = findView(tumbler);
mousePress(tumbler, tumblerXCenter(), tumblerYCenter());
// Ensure it's stationary.
var listView = tumblerView;
compare(listView.contentY, defaultListViewTumblerOffset);
// We could just move up until the contentY changed, but this is safer.
var distance = tumbler.height;
var changed = false;
for (var i = 0; i < distance && !changed; ++i) {
mouseMove(tumbler, tumblerXCenter(), tumblerYCenter() + i, 10);
// Don't test until the contentY has actually changed.
if (Math.abs(listView.contentY) - listView.preferredHighlightBegin > 0.01) {
for (var delegateIndex = 0; delegateIndex < Math.min(tumbler.count, tumbler.visibleItemCount); ++delegateIndex) {
var delegate = findChild(listView.contentItem, "delegate" + delegateIndex);
verify(delegate);
verify(delegate.displacement <= -delegateIndex, "Delegate at index " + delegateIndex + " has a displacement of "
+ delegate.displacement + " when it should be less than or equal to " + -delegateIndex);
verify(delegate.displacement > -delegateIndex - 0.1, "Delegate at index 0 has a displacement of "
+ delegate.displacement + " when it should be greater than ~ " + -delegateIndex - 0.1);
}
changed = true;
}
}
// Sanity check that something was actually tested.
verify(changed);
mouseRelease(tumbler, tumblerXCenter(), tumbler.topPadding);
}
property Component objectNameDelegate: Text {
objectName: "delegate" + index
text: modelData
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
function test_visibleItemCount_data() {
var data = [
// e.g. {0: 2} = {delegate index: y pos / delegate height}
// Skip item at index 3, because it's out of view.
{ model: 6, visibleItemCount: 5, expectedYPositions: {0: 2, 1: 3, 2: 4, 4: 0} },
{ model: 5, visibleItemCount: 3, expectedYPositions: {0: 1, 1: 2, 4: 0} },
// Takes up the whole view.
{ model: 2, visibleItemCount: 1, expectedYPositions: {0: 0} },
];
for (var i = 0; i < data.length; ++i) {
data[i].tag = "items=" + data[i].model + ", visibleItemCount=" + data[i].visibleItemCount;
}
return data;
}
function test_visibleItemCount(data) {
createTumbler();
tumbler.delegate = objectNameDelegate;
tumbler.visibleItemCount = data.visibleItemCount;
tumbler.model = data.model;
compare(tumbler.count, data.model);
for (var delegateIndex = 0; delegateIndex < data.visibleItemCount; ++delegateIndex) {
if (data.expectedYPositions.hasOwnProperty(delegateIndex)) {
var delegate = findChild(tumblerView, "delegate" + delegateIndex);
verify(delegate, "Delegate found at index " + delegateIndex);
var expectedYPos = data.expectedYPositions[delegateIndex] * tumblerDelegateHeight;
compare(delegate.mapToItem(tumbler.contentItem, 0, 0).y, expectedYPos);
}
}
}
property Component wrongDelegateTypeComponent: QtObject {
property real displacement: Tumbler.displacement
}
property Component noParentDelegateComponent: Item {
property real displacement: Tumbler.displacement
}
function test_attachedProperties() {
tumbler = createTemporaryObject(tumblerComponent, testCase);
verify(tumbler);
// TODO: crashes somewhere in QML's guts
// tumbler.model = 5;
// tumbler.delegate = wrongDelegateTypeComponent;
// ignoreWarning("Attached properties of Tumbler must be accessed from within a delegate item");
// // Cause displacement to be changed. The warning isn't triggered if we don't do this.
// tumbler.contentItem.offset += 1;
ignoreWarning("Tumbler: attached properties must be accessed through a delegate item that has a parent");
createTemporaryObject(noParentDelegateComponent, null);
ignoreWarning("Tumbler: attempting to access attached property on item without an \"index\" property");
var object = createTemporaryObject(noParentDelegateComponent, testCase);
verify(object);
}
property Component paddingDelegate: Text {
objectName: "delegate" + index
text: modelData
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: "red"
border.width: 1
}
}
function test_padding_data() {
var data = [];
data.push({ padding: 0 });
data.push({ padding: 10 });
data.push({ left: 10, top: 10 });
data.push({ right: 10, bottom: 10 });
for (var i = 0; i < data.length; ++i) {
var tag = "";
if (data[i].padding !== undefined)
tag += "padding: " + data[i].padding + " ";
if (data[i].left !== undefined)
tag += "left: " + data[i].left + " ";
if (data[i].right !== undefined)
tag += "right: " + data[i].right + " ";
if (data[i].top !== undefined)
tag += "top: " + data[i].top + " ";
if (data[i].bottom !== undefined)
tag += "bottom: " + data[i].bottom + " ";
tag = tag.slice(0, -1);
data[i].tag = tag;
}
return data;
}
function test_padding(data) {
createTumbler();
tumbler.delegate = paddingDelegate;
tumbler.model = 5;
compare(tumbler.padding, 0);
compare(tumbler.leftPadding, 0);
compare(tumbler.rightPadding, 0);
compare(tumbler.topPadding, 0);
compare(tumbler.bottomPadding, 0);
compare(tumbler.contentItem.x, 0);
compare(tumbler.contentItem.y, 0);
if (data.padding !== undefined)
tumbler.padding = data.padding;
if (data.left !== undefined)
tumbler.leftPadding = data.left;
if (data.right !== undefined)
tumbler.rightPadding = data.right;
if (data.top !== undefined)
tumbler.topPadding = data.top;
if (data.bottom !== undefined)
tumbler.bottomPadding = data.bottom;
compare(tumbler.availableWidth, tumbler.implicitWidth - tumbler.leftPadding - tumbler.rightPadding);
compare(tumbler.availableHeight, tumbler.implicitHeight - tumbler.topPadding - tumbler.bottomPadding);
compare(tumbler.contentItem.x, tumbler.leftPadding);
compare(tumbler.contentItem.y, tumbler.topPadding);
var pathView = tumbler.contentItem;
var expectedDelegateHeight = tumbler.availableHeight / tumbler.visibleItemCount;
var itemIndicesInVisualOrder = [4, 0, 1];
for (var i = 0; i < itemIndicesInVisualOrder.length; ++i) {
var delegate = findChild(pathView, "delegate" + itemIndicesInVisualOrder[i]);
verify(delegate, "Couldn't find delegate at index " + itemIndicesInVisualOrder[i]
+ " (iteration " + i + " out of " + (pathView.children.length - 1) + ")");
compare(delegate.width, tumbler.availableWidth);
compare(delegate.height, expectedDelegateHeight);
var expectedY = tumbler.topPadding + i * expectedDelegateHeight;
var mappedPos = delegate.mapToItem(null, delegate.width / 2, 0);
fuzzyCompare(mappedPos.y, expectedY, 0.5,
"Tumbler's PathView delegate at index " + itemIndicesInVisualOrder[i]
+ " should have a y pos of " + expectedY + ", but it's actually " + mappedPos.y.toFixed(20));
var expectedX = tumbler.leftPadding;
compare(delegate.mapToItem(null, 0, 0).x, expectedX,
"Tumbler's PathView delegate at index " + itemIndicesInVisualOrder[i]
+ " should have a x pos of " + expectedX + ", but it's actually " + mappedPos.x.toFixed(20));
}
// Force new items to be created, as there was a bug where the path was correct until this happened.
compare(tumblerView.offset, 0);
++tumbler.currentIndex;
tryCompare(tumblerView, "offset", 4, tumblerView.highlightMoveDuration * 2);
}
function test_moving_data() {
return [
{ tag: "wrap:true", wrap: true },
{ tag: "wrap:false", wrap: false }
]
}
function test_moving(data) {
createTumbler({wrap: data.wrap, model: 5})
compare(tumbler.wrap, data.wrap)
compare(tumbler.moving, false)
waitForRendering(tumbler)
mousePress(tumbler, tumbler.width / 2, tumbler.height / 2, Qt.LeftButton)
compare(tumbler.moving, false)
for (var y = tumbler.height / 2; y >= tumbler.height / 4; y -= 10)
mouseMove(tumbler, tumbler.width / 2, y, 1)
compare(tumbler.moving, true)
mouseRelease(tumbler, tumbler.width / 2, tumbler.height / 4, Qt.LeftButton)
compare(tumbler.moving, true)
tryCompare(tumbler, "moving", false)
}
Component {
id: qtbug61374Component
Row {
property alias tumbler: tumbler
property alias label: label
Component.onCompleted: {
tumbler.currentIndex = 2
}
Tumbler {
id: tumbler
model: 5
// ...
}
Label {
id: label
text: tumbler.currentItem.text
}
}
}
function test_qtbug61374() {
var row = createTemporaryObject(qtbug61374Component, testCase);
verify(row);
var tumbler = row.tumbler;
tryCompare(tumbler, "currentIndex", 2);
tumblerView = findView(tumbler);
var label = row.label;
compare(label.text, "2");
}
function test_positionViewAtIndex_data() {
return [
// Should be 20, 21, ... but there is a documented limitation for this in positionViewAtIndex()'s docs.
{ tag: "wrap=true, mode=Beginning", wrap: true, mode: Tumbler.Beginning, expectedVisibleIndices: [21, 22, 23, 24, 25] },
{ tag: "wrap=true, mode=Center", wrap: true, mode: Tumbler.Center, expectedVisibleIndices: [18, 19, 20, 21, 22] },
{ tag: "wrap=true, mode=End", wrap: true, mode: Tumbler.End, expectedVisibleIndices: [16, 17, 18, 19, 20] },
// Same as Beginning; should start at 20.
{ tag: "wrap=true, mode=Contain", wrap: true, mode: Tumbler.Contain, expectedVisibleIndices: [21, 22, 23, 24, 25] },
{ tag: "wrap=true, mode=SnapPosition", wrap: true, mode: Tumbler.SnapPosition, expectedVisibleIndices: [18, 19, 20, 21, 22] },
{ tag: "wrap=false, mode=Beginning", wrap: false, mode: Tumbler.Beginning, expectedVisibleIndices: [20, 21, 22, 23, 24] },
{ tag: "wrap=false, mode=Center", wrap: false, mode: Tumbler.Center, expectedVisibleIndices: [18, 19, 20, 21, 22] },
{ tag: "wrap=false, mode=End", wrap: false, mode: Tumbler.End, expectedVisibleIndices: [16, 17, 18, 19, 20] },
{ tag: "wrap=false, mode=Visible", wrap: false, mode: Tumbler.Visible, expectedVisibleIndices: [16, 17, 18, 19, 20] },
{ tag: "wrap=false, mode=Contain", wrap: false, mode: Tumbler.Contain, expectedVisibleIndices: [16, 17, 18, 19, 20] },
{ tag: "wrap=false, mode=SnapPosition", wrap: false, mode: Tumbler.SnapPosition, expectedVisibleIndices: [18, 19, 20, 21, 22] }
]
}
function test_positionViewAtIndex(data) {
createTumbler({ wrap: data.wrap, model: 40, visibleItemCount: 5 })
compare(tumbler.wrap, data.wrap)
waitForRendering(tumbler)
tumbler.positionViewAtIndex(20, data.mode)
tryCompare(tumbler, "moving", false)
compare(tumbler.visibleItemCount, 5)
for (var i = 0; i < 5; ++i) {
// Find the item through its text, as that's easier than child/itemAt().
var text = data.expectedVisibleIndices[i].toString()
var item = findDelegateWithText(tumblerView, text)
verify(item, "found no item with text \"" + text + "\"")
compare(item.text, data.expectedVisibleIndices[i].toString())
// Ensure that it's at the position we expect.
var expectedPos = itemTopLeftPos(i)
var actualPos = testCase.mapFromItem(item, 0, 0)
compare(actualPos.x, expectedPos.x, "expected delegate with text " + item.text
+ " to have an x pos of " + expectedPos.x + " but it was " + actualPos.x)
compare(actualPos.y, expectedPos.y, "expected delegate with text " + item.text
+ " to have an y pos of " + expectedPos.y + " but it was " + actualPos.y)
}
}
Component {
id: setCurrentIndexOnImperativeModelChangeComponent
Tumbler {
onModelChanged: currentIndex = model - 2
}
}
function test_setCurrentIndexOnImperativeModelChange() {
var tumbler = createTemporaryObject(setCurrentIndexOnImperativeModelChangeComponent, testCase);
verify(tumbler);
tumbler.model = 4
compare(tumbler.count, 4);
tumblerView = findView(tumbler);
tryCompare(tumblerView, "count", 4);
// 4 - 2 = 2
compare(tumbler.currentIndex, 2);
++tumbler.model;
compare(tumbler.count, 5);
compare(tumbler.wrap, true);
tumblerView = findView(tumbler);
tryCompare(tumblerView, "count", 5);
// 5 - 2 = 3
compare(tumbler.currentIndex, 3);
}
Component {
id: setCurrentIndexOnDeclarativeModelChangeComponent
Item {
property alias tumbler: tumbler
property int setting: 4
Tumbler {
id: tumbler
model: setting
onModelChanged: currentIndex = model - 2
}
}
}
function test_setCurrentIndexOnDeclarativeModelChange() {
var root = createTemporaryObject(setCurrentIndexOnDeclarativeModelChangeComponent, testCase);
verify(root);
var tumbler = root.tumbler;
compare(tumbler.count, 4);
compare(tumbler.wrap, false);
tumblerView = findView(tumbler);
tryCompare(tumblerView, "count", 4);
// 4 - 2 = 2
compare(tumbler.currentIndex, 2);
++root.setting;
compare(tumbler.count, 5);
compare(tumbler.wrap, true);
tumblerView = findView(tumbler);
tryCompare(tumblerView, "count", 5);
// 5 - 2 = 3
compare(tumbler.currentIndex, 3);
}
function test_displacementAfterResizing() {
createTumbler({
width: 200,
wrap: false,
delegate: displacementDelegate,
model: 30,
visibleItemCount: 7,
currentIndex: 15
})
var delegate = findChild(tumblerView, "delegate15")
verify(delegate)
tryCompare(delegate, "displacement", 0)
// Resizing the Tumbler shouldn't affect the displacement.
tumbler.height *= 1.4
tryCompare(delegate, "displacement", 0)
}
}