/****************************************************************************
**
** Copyright (C) 2017 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: 200
    height: 200
    visible: true
    when: windowShown
    name: "SwipeDelegate"

    readonly property int dragDistance: Math.max(20, Qt.styleHints.startDragDistance + 5)

    Component {
        id: backgroundFillComponent
        SwipeDelegate {
            background: Item { anchors.fill: parent }
        }
    }

    Component {
        id: backgroundCenterInComponent
        SwipeDelegate {
            background: Item { anchors.centerIn: parent }
        }
    }

    Component {
        id: backgroundLeftComponent
        SwipeDelegate {
            background: Item { anchors.left: parent.left }
        }
    }

    Component {
        id: backgroundRightComponent
        SwipeDelegate {
            background: Item { anchors.right: parent.right }
        }
    }

    Component {
        id: contentItemFillComponent
        SwipeDelegate {
            contentItem: Item { anchors.fill: parent }
        }
    }

    Component {
        id: contentItemCenterInComponent
        SwipeDelegate {
            contentItem: Item { anchors.centerIn: parent }
        }
    }

    Component {
        id: contentItemLeftComponent
        SwipeDelegate {
            contentItem: Item { anchors.left: parent.left }
        }
    }

    Component {
        id: contentItemRightComponent
        SwipeDelegate {
            contentItem: Item { anchors.right: parent.right }
        }
    }

    function test_horizontalAnchors_data() {
        return [
            { tag: "background, fill", component: backgroundFillComponent, itemName: "background", warningLocation: ":69:25" },
            { tag: "background, centerIn", component: backgroundCenterInComponent, itemName: "background", warningLocation: ":76:25" },
            { tag: "background, left", component: backgroundLeftComponent, itemName: "background", warningLocation: ":83:25" },
            { tag: "background, right", component: backgroundRightComponent, itemName: "background", warningLocation: ":90:25" },
            { tag: "contentItem, fill", component: contentItemFillComponent, itemName: "contentItem", warningLocation: ":97:26" },
            { tag: "contentItem, centerIn", component: contentItemCenterInComponent, itemName: "contentItem", warningLocation: ":104:26" },
            { tag: "contentItem, left", component: contentItemLeftComponent, itemName: "contentItem", warningLocation: ":111:26" },
            { tag: "contentItem, right", component: contentItemRightComponent, itemName: "contentItem", warningLocation: ":118:26" }
        ];
    }

    function test_horizontalAnchors(data) {
        var warningMessage = Qt.resolvedUrl("tst_swipedelegate.qml") + data.warningLocation
            + ": QML QQuickItem: SwipeDelegate: cannot use horizontal anchors with " + data.itemName + "; unable to layout the item."

        ignoreWarning(warningMessage);

        var control = createTemporaryObject(data.component, testCase);
        verify(control.contentItem);
    }

    Component {
        id: greenLeftComponent

        Rectangle {
            objectName: "leftItem"
            anchors.fill: parent
            color: "green"
        }
    }

    Component {
        id: redRightComponent

        Rectangle {
            objectName: "rightItem"
            anchors.fill: parent
            color: "red"
        }
    }

    Component {
        id: swipeDelegateComponent

        SwipeDelegate {
            id: swipeDelegate
            text: "SwipeDelegate"
            width: 150
            swipe.left: greenLeftComponent
            swipe.right: redRightComponent
        }
    }

    Component {
        id: signalSpyComponent

        SignalSpy {}
    }

    Component {
        id: itemComponent

        Item {}
    }

    // Assumes that the delegate is smaller than the width of the control.
    function swipe(control, from, to) {
        // Sanity check.
        compare(control.swipe.position, from);

        var distance = (to - from) * control.width;

        mousePress(control, control.width / 2, control.height / 2, Qt.LeftButton);
        mouseMove(control, control.width / 2 + distance, control.height / 2);
        mouseRelease(control, control.width / 2 + distance, control.height / 2, Qt.LeftButton);
        compare(control.swipe.position, to, "Expected swipe.position to be " + to
            + " after swiping from " + from + ", but it's " + control.swipe.position);

        if (control.swipe.position === -1.0) {
            if (control.swipe.right)
                verify(control.swipe.rightItem);
            else if (control.swipe.behind)
                verify(control.swipe.behindItem);
        } else if (control.swipe.position === 1.0) {
            if (control.swipe.left)
                verify(control.swipe.leftItem);
            else if (control.swipe.behind)
                verify(control.swipe.behindItem);
        }
    }

    function test_settingDelegates() {
        var control = createTemporaryObject(swipeDelegateComponent, testCase);
        verify(control);

        ignoreWarning(Qt.resolvedUrl("tst_swipedelegate.qml") +
            ":168:9: QML SwipeDelegate: cannot set both behind and left/right properties")
        control.swipe.behind = itemComponent;

        // Shouldn't be any warnings when unsetting delegates.
        control.swipe.left = null;
        compare(control.swipe.leftItem, null);

        // right is still set.
        ignoreWarning(Qt.resolvedUrl("tst_swipedelegate.qml") +
            ":168:9: QML SwipeDelegate: cannot set both behind and left/right properties")
        control.swipe.behind = itemComponent;

        control.swipe.right = null;
        compare(control.swipe.rightItem, null);

        control.swipe.behind = itemComponent;

        ignoreWarning(Qt.resolvedUrl("tst_swipedelegate.qml") +
            ":168:9: QML SwipeDelegate: cannot set both behind and left/right properties")
        control.swipe.left = itemComponent;

        ignoreWarning(Qt.resolvedUrl("tst_swipedelegate.qml") +
            ":168:9: QML SwipeDelegate: cannot set both behind and left/right properties")
        control.swipe.right = itemComponent;

        control.swipe.behind = null;
        control.swipe.left = greenLeftComponent;
        control.swipe.right = redRightComponent;

        // Test that the user is warned when attempting to set or unset left or
        // right item while they're exposed.
        // First, try the left item.
        swipe(control, 0.0, 1.0);

        var oldLeft = control.swipe.left;
        var oldLeftItem = control.swipe.leftItem;
        ignoreWarning(Qt.resolvedUrl("tst_swipedelegate.qml") +
            ":168:9: QML SwipeDelegate: left/right/behind properties may only be set when swipe.position is 0")
        control.swipe.left = null;
        compare(control.swipe.left, oldLeft);
        compare(control.swipe.leftItem, oldLeftItem);

        // Try the same thing with the right item.
        swipe(control, 1.0, -1.0);

        var oldRight = control.swipe.right;
        var oldRightItem = control.swipe.rightItem;
        ignoreWarning(Qt.resolvedUrl("tst_swipedelegate.qml") +
            ":168:9: QML SwipeDelegate: left/right/behind properties may only be set when swipe.position is 0")
        control.swipe.right = null;
        compare(control.swipe.right, oldRight);
        compare(control.swipe.rightItem, oldRightItem);

        // Return to the default position.
        swipe(control, -1.0, 0.0);

        tryCompare(control.background, "x", 0, 1000);

        // Try the same thing with the behind item.
        control.swipe.left = null;
        verify(!control.swipe.left);
        verify(!control.swipe.leftItem);
        control.swipe.right = null;
        verify(!control.swipe.right);
        verify(!control.swipe.rightItem);
        control.swipe.behind = greenLeftComponent;
        verify(control.swipe.behind);
        verify(!control.swipe.behindItem);

        swipe(control, 0.0, 1.0);

        var oldBehind = control.swipe.behind;
        var oldBehindItem = control.swipe.behindItem;
        ignoreWarning(Qt.resolvedUrl("tst_swipedelegate.qml") +
            ":168:9: QML SwipeDelegate: left/right/behind properties may only be set when swipe.position is 0")
        control.swipe.behind = null;
        compare(control.swipe.behind, oldBehind);
        compare(control.swipe.behindItem, oldBehindItem);
    }

    function test_defaults() {
        var control = createTemporaryObject(swipeDelegateComponent, testCase);
        verify(control);

        compare(control.baselineOffset, control.contentItem.y + control.contentItem.baselineOffset);
        compare(control.swipe.position, 0);
        verify(!control.pressed);
        verify(!control.swipe.complete);
    }

    SignalSequenceSpy {
        id: mouseSignalSequenceSpy
        signals: ["pressed", "released", "canceled", "clicked", "doubleClicked", "pressedChanged", "pressAndHold"]
    }

    function test_swipe() {
        var control = createTemporaryObject(swipeDelegateComponent, testCase);
        verify(control);

        var overDragDistance = Math.round(dragDistance * 1.1);

        var completedSpy = signalSpyComponent.createObject(control, { target: control.swipe, signalName: "completed" });
        verify(completedSpy);
        verify(completedSpy.valid);

        var openedSpy = signalSpyComponent.createObject(control, { target: control.swipe, signalName: "opened" });
        verify(openedSpy);
        verify(openedSpy.valid);

        var closedSpy = signalSpyComponent.createObject(control, { target: control.swipe, signalName: "closed" });
        verify(closedSpy);
        verify(closedSpy.valid);

        mouseSignalSequenceSpy.target = control;
        mouseSignalSequenceSpy.expectedSequence = [["pressedChanged", { "pressed": true }], "pressed"];
        mousePress(control, control.width / 2, control.height / 2);
        verify(control.pressed);
        compare(control.swipe.position, 0.0);
        verify(!control.swipe.complete);
        compare(completedSpy.count, 0);
        compare(openedSpy.count, 0);
        compare(closedSpy.count, 0);
        verify(mouseSignalSequenceSpy.success);
        verify(!control.swipe.leftItem);
        verify(!control.swipe.rightItem);

        // Drag to the right so that leftItem is created and visible.
        mouseMove(control, control.width / 2 + overDragDistance, control.height / 2);
        verify(control.pressed);
        compare(control.swipe.position, overDragDistance / control.width);
        verify(!control.swipe.complete);
        compare(completedSpy.count, 0);
        compare(openedSpy.count, 0);
        compare(closedSpy.count, 0);
        verify(control.swipe.leftItem);
        verify(control.swipe.leftItem.visible);
        compare(control.swipe.leftItem.parent, control);
        compare(control.swipe.leftItem.objectName, "leftItem");
        verify(!control.swipe.rightItem);

        // Go back to 0.
        mouseMove(control, control.width / 2, control.height / 2);
        verify(control.pressed);
        compare(control.swipe.position, 0.0);
        verify(!control.swipe.complete);
        compare(completedSpy.count, 0);
        compare(openedSpy.count, 0);
        compare(closedSpy.count, 0);
        verify(control.swipe.leftItem);
        verify(control.swipe.leftItem.visible);
        compare(control.swipe.leftItem.parent, control);
        compare(control.swipe.leftItem.objectName, "leftItem");
        verify(!control.swipe.rightItem);

        // Try the other direction. The right item should be created and visible,
        // and the left item should be hidden.
        mouseMove(control, control.width / 2 - overDragDistance, control.height / 2);
        verify(control.pressed);
        compare(control.swipe.position, -overDragDistance / control.width);
        verify(!control.swipe.complete);
        compare(completedSpy.count, 0);
        compare(openedSpy.count, 0);
        compare(closedSpy.count, 0);
        verify(control.swipe.leftItem);
        verify(!control.swipe.leftItem.visible);
        verify(control.swipe.rightItem);
        verify(control.swipe.rightItem.visible);
        compare(control.swipe.rightItem.parent, control);
        compare(control.swipe.rightItem.objectName, "rightItem");

        // Now release outside the right edge of the control.
        mouseMove(control, control.width * 1.1, control.height / 2);
        verify(control.pressed);
        compare(control.swipe.position, 0.6);
        verify(!control.swipe.complete);
        compare(completedSpy.count, 0);
        compare(openedSpy.count, 0);
        compare(closedSpy.count, 0);
        verify(control.swipe.leftItem);
        verify(control.swipe.leftItem.visible);
        verify(control.swipe.rightItem);
        verify(!control.swipe.rightItem.visible);

        mouseSignalSequenceSpy.expectedSequence = [["pressedChanged", { "pressed": false }], "canceled"];
        mouseRelease(control, control.width / 2, control.height / 2);
        verify(!control.pressed);
        tryCompare(control.swipe, "position", 1.0);
        tryCompare(control.swipe, "complete", true);
        compare(completedSpy.count, 1);
        compare(openedSpy.count, 1);
        compare(closedSpy.count, 0);
        verify(mouseSignalSequenceSpy.success);
        verify(control.swipe.leftItem);
        verify(control.swipe.leftItem.visible);
        verify(control.swipe.rightItem);
        verify(!control.swipe.rightItem.visible);
        tryCompare(control.contentItem, "x", control.width + control.leftPadding);

        // Swiping from the right and releasing early should return position to 1.0.
        mouseSignalSequenceSpy.expectedSequence = [["pressedChanged", { "pressed": true }], "pressed"];
        mousePress(control, control.width / 2, control.height / 2);
        verify(control.pressed);
        compare(control.swipe.position, 1.0);
        // complete should still be true, because we haven't moved yet, and hence
        // haven't started grabbing behind's mouse events.
        verify(control.swipe.complete);
        compare(completedSpy.count, 1);
        compare(openedSpy.count, 1);
        compare(closedSpy.count, 0);
        verify(mouseSignalSequenceSpy.success);

        mouseMove(control, control.width / 2 - overDragDistance, control.height / 2);
        verify(control.pressed);
        verify(!control.swipe.complete);
        compare(completedSpy.count, 1);
        compare(openedSpy.count, 1);
        compare(closedSpy.count, 0);
        compare(control.swipe.position, 1.0 - overDragDistance / control.width);

        // Since we went over the drag distance, we should expect canceled() to be emitted.
        mouseSignalSequenceSpy.expectedSequence = [["pressedChanged", { "pressed": false }], "canceled"];
        mouseRelease(control, control.width * 0.4, control.height / 2);
        verify(!control.pressed);
        tryCompare(control.swipe, "position", 1.0);
        tryCompare(control.swipe, "complete", true);
        compare(completedSpy.count, 2);
        compare(openedSpy.count, 2);
        compare(closedSpy.count, 0);
        verify(mouseSignalSequenceSpy.success);
        tryCompare(control.contentItem, "x", control.width + control.leftPadding);

        // Swiping from the right and releasing should return contents to default position.
        mouseSignalSequenceSpy.expectedSequence = [["pressedChanged", { "pressed": true }], "pressed"];
        mousePress(control, control.width / 2, control.height / 2);
        verify(control.pressed);
        compare(control.swipe.position, 1.0);
        verify(control.swipe.complete);
        compare(completedSpy.count, 2);
        compare(openedSpy.count, 2);
        compare(closedSpy.count, 0);
        verify(mouseSignalSequenceSpy.success);

        mouseMove(control, control.width * -0.1, control.height / 2);
        verify(control.pressed);
        verify(!control.swipe.complete);
        compare(completedSpy.count, 2);
        compare(openedSpy.count, 2);
        compare(closedSpy.count, 0);
        compare(control.swipe.position, 0.4);

        mouseSignalSequenceSpy.expectedSequence = [["pressedChanged", { "pressed": false }], "canceled"];
        mouseRelease(control, control.width * -0.1, control.height / 2);
        verify(!control.pressed);
        tryCompare(control.swipe, "position", 0.0);
        verify(!control.swipe.complete);
        compare(completedSpy.count, 2);
        compare(openedSpy.count, 2);
        tryCompare(closedSpy, "count", 1);
        verify(mouseSignalSequenceSpy.success);
        tryCompare(control.contentItem, "x", control.leftPadding);
    }

    function test_swipeVelocity_data() {
        return [
            { tag: "positive velocity", direction: 1 },
            { tag: "negative velocity", direction: -1 }
        ];
    }

    function test_swipeVelocity(data) {
        skip("QTBUG-52003");

        var control = createTemporaryObject(swipeDelegateComponent, testCase);
        verify(control);

        var distance = Math.round(dragDistance * 1.1);
        if (distance >= control.width / 2)
            skip("This test requires a startDragDistance that is less than half the width of the control");

        distance *= data.direction;

        mouseSignalSequenceSpy.target = control;
        mouseSignalSequenceSpy.expectedSequence = [["pressedChanged", { "pressed": true }], "pressed"];
        mousePress(control, control.width / 2, control.height / 2);
        verify(control.pressed);
        compare(control.swipe.position, 0.0);
        verify(!control.swipe.complete);
        verify(mouseSignalSequenceSpy.success);
        verify(!control.swipe.leftItem);
        verify(!control.swipe.rightItem);

        // Swipe quickly to the side over a distance that is longer than the drag threshold,
        // quicker than the expose velocity threshold, but shorter than the halfway mark.
        mouseMove(control, control.width / 2 + distance, control.height / 2);
        verify(control.pressed);
        compare(control.swipe.position, distance / control.width);
        verify(control.swipe.position < 0.5);
        verify(!control.swipe.complete);

        var expectedVisibleItem;
        var expectedVisibleObjectName;
        var expectedHiddenItem;
        var expectedContentItemX;
        if (distance > 0) {
            expectedVisibleObjectName = "leftItem";
            expectedVisibleItem = control.swipe.leftItem;
            expectedHiddenItem = control.swipe.rightItem;
            expectedContentItemX = control.width + control.leftPadding;
        } else {
            expectedVisibleObjectName = "rightItem";
            expectedVisibleItem = control.swipe.rightItem;
            expectedHiddenItem = control.swipe.leftItem;
            expectedContentItemX = -control.width + control.leftPadding;
        }
        verify(expectedVisibleItem);
        verify(expectedVisibleItem.visible);
        compare(expectedVisibleItem.parent, control);
        compare(expectedVisibleItem.objectName, expectedVisibleObjectName);
        verify(!expectedHiddenItem);

        mouseSignalSequenceSpy.expectedSequence = [["pressedChanged", { "pressed": false }], "released", "clicked"];
        // Add a delay to ensure that the release event doesn't happen too quickly,
        // and hence that the second timestamp isn't zero (can happen with e.g. release builds).
        mouseRelease(control, control.width / 2 + distance, control.height / 2, Qt.LeftButton, Qt.NoModifier, 30);
        verify(!control.pressed);
        compare(control.swipe.position, data.direction);
        verify(control.swipe.complete);
        verify(mouseSignalSequenceSpy.success);
        verify(expectedVisibleItem);
        verify(expectedVisibleItem.visible);
        verify(!expectedHiddenItem);
        tryCompare(control.contentItem, "x", expectedContentItemX);
    }

    Component {
        id: swipeDelegateWithButtonComponent
        SwipeDelegate {
            text: "SwipeDelegate"
            width: 150
            swipe.right: Button {
                width: parent.width
                height: parent.height
                text: "Boo!"
            }
        }
    }

    function test_eventsToLeftAndRight() {
        var control = createTemporaryObject(swipeDelegateWithButtonComponent, testCase);
        verify(control);

        var closedSpy = signalSpyComponent.createObject(control, { target: control.swipe, signalName: "closed" });
        verify(closedSpy);
        verify(closedSpy.valid);

        // The button should be pressed instead of the SwipeDelegate.
        mouseDrag(control, control.width / 2, 0,  -control.width, 0);
        // Mouse has been released by this stage.
        verify(!control.pressed);
        compare(control.swipe.position, -1.0);
        verify(control.swipe.rightItem);
        verify(control.swipe.rightItem.visible);
        compare(control.swipe.rightItem.parent, control);

        var buttonPressedSpy = signalSpyComponent.createObject(control, { target: control.swipe.rightItem, signalName: "pressed" });
        verify(buttonPressedSpy);
        verify(buttonPressedSpy.valid);
        var buttonReleasedSpy = signalSpyComponent.createObject(control, { target: control.swipe.rightItem, signalName: "released" });
        verify(buttonReleasedSpy);
        verify(buttonReleasedSpy.valid);
        var buttonClickedSpy = signalSpyComponent.createObject(control, { target: control.swipe.rightItem, signalName: "clicked" });
        verify(buttonClickedSpy);
        verify(buttonClickedSpy.valid);

        // Now press the button.
        mousePress(control, control.width / 2, control.height / 2);
        verify(!control.pressed);
        var button = control.swipe.rightItem;
        verify(button.pressed);
        compare(buttonPressedSpy.count, 1);
        compare(buttonReleasedSpy.count, 0);
        compare(buttonClickedSpy.count, 0);

        mouseRelease(control, control.width / 2, control.height / 2);
        verify(!button.pressed);
        compare(buttonPressedSpy.count, 1);
        compare(buttonReleasedSpy.count, 1);
        compare(buttonClickedSpy.count, 1);

        // Returning back to a position of 0 and pressing on the control should
        // result in the control being pressed.
        mouseDrag(control, control.width / 2, 0, control.width * 0.6, 0);
        tryCompare(closedSpy, "count", 1);
        compare(control.swipe.position, 0);
        mousePress(control, control.width / 2, control.height / 2);
        verify(control.pressed);
        verify(!button.pressed);
        mouseRelease(control, control.width / 2, control.height / 2);
        verify(!control.pressed);
    }

    function test_mouseButtons() {
        var control = createTemporaryObject(swipeDelegateComponent, testCase);
        verify(control);

        // click
        mouseSignalSequenceSpy.target = control;
        mouseSignalSequenceSpy.expectedSequence = [["pressedChanged", { "pressed": true }], "pressed"];
        mousePress(control, control.width / 2, control.height / 2, Qt.LeftButton);
        compare(control.pressed, true);

        verify(mouseSignalSequenceSpy.success);

        mouseSignalSequenceSpy.expectedSequence = [["pressedChanged", { "pressed": false }], "released", "clicked"];
        mouseRelease(control, control.width / 2, control.height / 2, Qt.LeftButton);
        compare(control.pressed, false);
        verify(mouseSignalSequenceSpy.success);

        // right button
        mouseSignalSequenceSpy.expectedSequence = [];
        mousePress(control, control.width / 2, control.height / 2, Qt.RightButton);
        compare(control.pressed, false);

        mouseRelease(control, control.width / 2, control.height / 2, Qt.RightButton);
        compare(control.pressed, false);
        verify(mouseSignalSequenceSpy.success);

        // double click
        mouseSignalSequenceSpy.expectedSequence = [
            ["pressedChanged", { "pressed": true }],
            "pressed",
            ["pressedChanged", { "pressed": false }],
            "released",
            "clicked",
            ["pressedChanged", { "pressed": true }],
            "pressed",
            "doubleClicked",
            ["pressedChanged", { "pressed": false }],
            "released"
        ];
        mouseDoubleClickSequence(control, control.width / 2, control.height / 2, Qt.LeftButton);
        verify(mouseSignalSequenceSpy.success);

        // press and hold
        var pressAndHoldSpy = signalSpyComponent.createObject(control, { target: control, signalName: "pressAndHold" });
        verify(pressAndHoldSpy);
        verify(pressAndHoldSpy.valid);

        mouseSignalSequenceSpy.expectedSequence = [
            ["pressedChanged", { "pressed": true }],
            "pressed",
            "pressAndHold",
            ["pressedChanged", { "pressed": false }],
            "released"
        ];
        mousePress(control);
        compare(control.pressed, true);
        tryCompare(pressAndHoldSpy, "count", 1);

        mouseRelease(control);
        compare(control.pressed, false);
        verify(mouseSignalSequenceSpy.success);
    }

    Component {
        id: removableDelegatesComponent

        ListView {
            id: listView
            width: 100
            height: 120

            model: ListModel {
                ListElement { name: "Apple" }
                ListElement { name: "Orange" }
                ListElement { name: "Pear" }
            }

            delegate: SwipeDelegate {
                id: rootDelegate
                text: modelData
                width: listView.width

                property alias removeAnimation: onRemoveAnimation

                ListView.onRemove: onRemoveAnimation.start()

                SequentialAnimation {
                    id: onRemoveAnimation

                    PropertyAction {
                        target: rootDelegate
                        property: "ListView.delayRemove"
                        value: true
                    }
                    NumberAnimation {
                        target: rootDelegate
                        property: "height"
                        to: 0
                        easing.type: Easing.InOutQuad
                    }
                    PropertyAction {
                        target: rootDelegate;
                        property: "ListView.delayRemove";
                        value: false
                    }
                }

                swipe.left: Rectangle {
                    objectName: "rectangle"
                    color: SwipeDelegate.pressed ? "#333" : "#444"
                    anchors.fill: parent

                    SwipeDelegate.onClicked: listView.model.remove(index)

                    Label {
                        objectName: "label"
                        text: "Remove"
                        color: "white"
                        anchors.centerIn: parent
                    }
                }
            }
        }
    }

    function test_removableDelegates() {
        var listView = createTemporaryObject(removableDelegatesComponent, testCase);
        verify(listView);
        compare(listView.count, 3);

        // Expose the remove button.
        var firstItem = listView.itemAt(0, 0);
        mousePress(listView, firstItem.width / 2, firstItem.height / 2);
        verify(firstItem.pressed);
        compare(firstItem.swipe.position, 0.0);
        verify(!firstItem.swipe.complete);
        verify(!firstItem.swipe.leftItem);

        mouseMove(listView, firstItem.width * 1.1, firstItem.height / 2);
        verify(firstItem.pressed);
        compare(firstItem.swipe.position, 0.6);
        verify(!firstItem.swipe.complete);
        verify(firstItem.swipe.leftItem);
        verify(!firstItem.swipe.leftItem.SwipeDelegate.pressed);

        mouseRelease(listView, firstItem.width / 2, firstItem.height / 2);
        verify(!firstItem.pressed);
        tryCompare(firstItem.swipe, "position", 1.0);
        tryCompare(firstItem.swipe, "complete", true);
        compare(listView.count, 3);

        // Wait for it to settle down.
        tryCompare(firstItem.contentItem, "x", firstItem.leftPadding + firstItem.width);

        var leftClickedSpy = signalSpyComponent.createObject(firstItem.swipe.leftItem,
            { target: firstItem.swipe.leftItem.SwipeDelegate, signalName: "clicked" });
        verify(leftClickedSpy);
        verify(leftClickedSpy.valid);

        // Click the left item to remove the delegate from the list.
        var contentItemX = firstItem.contentItem.x;
        mousePress(listView, firstItem.width / 2, firstItem.height / 2);
        verify(firstItem.swipe.leftItem.SwipeDelegate.pressed);
        compare(leftClickedSpy.count, 0);
        verify(!firstItem.pressed);

        mouseRelease(listView, firstItem.width / 2, firstItem.height / 2);
        verify(!firstItem.swipe.leftItem.SwipeDelegate.pressed);
        compare(leftClickedSpy.count, 1);
        verify(!firstItem.pressed);
        leftClickedSpy = null;
        tryCompare(firstItem.removeAnimation, "running", true);
        // There was a bug where the resizeContent() would be called because the height
        // of the control was changing due to the animation. contentItem would then
        // change x position and hence be visible when it shouldn't be.
        verify(firstItem.removeAnimation.running);
        while (1) {
            wait(10)
            if (firstItem && firstItem.removeAnimation && firstItem.removeAnimation.running)
                compare(firstItem.contentItem.x, contentItemX);
            else
                break;
        }
        compare(listView.count, 2);
    }

    Component {
        id: leadingTrailingXComponent
        SwipeDelegate {
            id: delegate
            width: 150
            text: "SwipeDelegate"

            swipe.left: Rectangle {
                x: delegate.background.x - width
                width: delegate.width
                height: delegate.height
                color: "green"
            }

            swipe.right: Rectangle {
                x: delegate.background.x + delegate.background.width
                width: delegate.width
                height: delegate.height
                color: "red"
            }
        }
    }

    Component {
        id: leadingTrailingAnchorsComponent
        SwipeDelegate {
            id: delegate
            width: 150
            text: "SwipeDelegate"

            swipe.left: Rectangle {
                anchors.right: delegate.background.left
                width: delegate.width
                height: delegate.height
                color: "green"
            }

            swipe.right: Rectangle {
                anchors.left: delegate.background.right
                width: delegate.width
                height: delegate.height
                color: "red"
            }
        }
    }

    function test_leadingTrailing_data() {
        return [
            { tag: "x", component: leadingTrailingXComponent },
            { tag: "anchors", component: leadingTrailingAnchorsComponent },
        ];
    }

    function test_leadingTrailing(data) {
        var control = createTemporaryObject(data.component, testCase);
        verify(control);

        mousePress(control, control.width / 2, control.height / 2, Qt.LeftButton);
        mouseMove(control, control.width, control.height / 2);
        verify(control.swipe.leftItem);
        compare(control.swipe.leftItem.x, -control.width / 2);
        mouseRelease(control, control.width / 2, control.height / 2, Qt.LeftButton);
    }

    function test_minMaxPosition() {
        var control = createTemporaryObject(leadingTrailingXComponent, testCase);
        verify(control);

        // Should be limited within the range -1.0 to 1.0.
        mousePress(control, control.width / 2, control.height / 2, Qt.LeftButton);
        mouseMove(control, control.width * 1.5, control.height / 2);
        compare(control.swipe.position, 1.0);
        mouseMove(control, control.width * 1.6, control.height / 2);
        compare(control.swipe.position, 1.0);
        mouseMove(control, control.width * -1.6, control.height / 2);
        compare(control.swipe.position, -1.0);
        mouseRelease(control, control.width / 2, control.height / 2, Qt.LeftButton);
    }

    Component {
        id: emptySwipeDelegateComponent

        SwipeDelegate {
            text: "SwipeDelegate"
            width: 150
        }
    }

    Component {
        id: smallLeftComponent

        Rectangle {
            width: 80
            height: 40
            color: "green"
        }
    }

    // swipe.position should be scaled to the width of the relevant delegate,
    // and it shouldn't be possible to drag past the delegate (so that content behind the control is visible).
    function test_delegateWidth() {
        var control = createTemporaryObject(emptySwipeDelegateComponent, testCase);
        verify(control);

        control.swipe.left = smallLeftComponent;

        // Ensure that the position is scaled to the width of the currently visible delegate.
        var overDragDistance = Math.round(dragDistance * 1.1);
        mousePress(control, control.width / 2, control.height / 2, Qt.LeftButton);
        mouseMove(control, control.width / 2 + overDragDistance, control.height / 2);
        verify(control.swipe.leftItem);
        compare(control.swipe.position, overDragDistance / control.swipe.leftItem.width);

        mouseMove(control, control.width / 2 + control.swipe.leftItem.width, control.height / 2);
        compare(control.swipe.position, 1.0);

        // Ensure that it's not possible to drag past the (left) delegate.
        mouseMove(control, control.width / 2 + control.swipe.leftItem.width + 1, control.height / 2);
        compare(control.swipe.position, 1.0);

        // Now release over the right side; the position should be 1.0 and the background
        // should be "anchored" to the right side of the left delegate item.
        mouseMove(control, control.width / 2 + control.swipe.leftItem.width, control.height / 2);
        mouseRelease(control, control.width / 2 + control.swipe.leftItem.width, control.height / 2, Qt.LeftButton);
        compare(control.swipe.position, 1.0);
        tryCompare(control.background, "x", control.swipe.leftItem.width, 1000);
    }

    SignalSpy {
        id: leftVisibleSpy
        signalName: "visibleChanged"
    }

    SignalSpy {
        id: rightVisibleSpy
        signalName: "visibleChanged"
    }

    function test_positionAfterSwipeCompleted() {
        var control = createTemporaryObject(swipeDelegateComponent, testCase);
        verify(control);

        // Ensure that both delegates are constructed.
        mousePress(control, 0, control.height / 2, Qt.LeftButton);
        mouseMove(control, control.width * 1.1, control.height / 2);
        verify(control.swipe.leftItem);
        mouseMove(control, control.width * -0.1, control.height / 2);
        verify(control.swipe.rightItem);

        // Expose the left delegate.
        mouseMove(control, control.swipe.leftItem.width, control.height / 2);
        mouseRelease(control, control.swipe.leftItem.width, control.height / 2);
        verify(control.swipe.complete);
        compare(control.swipe.position, 1.0);

        leftVisibleSpy.target = control.swipe.leftItem;
        rightVisibleSpy.target = control.swipe.rightItem;

        // Swipe from right to left without exposing the right item,
        // and make sure that the right item never becomes visible
        // (and hence that the left item never loses visibility).
        mousePress(control, control.swipe.leftItem.width, control.height / 2, Qt.LeftButton);
        compare(leftVisibleSpy.count, 0);
        compare(rightVisibleSpy.count, 0);
        var newX = control.swipe.leftItem.width - Math.round(dragDistance * 1.1);
        mouseMove(control, newX, control.height / 2);
        compare(leftVisibleSpy.count, 0);
        compare(rightVisibleSpy.count, 0);
        compare(control.swipe.position, newX / control.swipe.leftItem.width);

        mouseMove(control, 0, control.height / 2);
        compare(control.swipe.position, 0);

        // Test swiping over a distance that is greater than the width of the left item.
        mouseMove(control, -1, control.height / 2);
        verify(control.swipe.rightItem);
        compare(control.swipe.position, -1 / control.swipe.rightItem.width);

        // Now go back to 1.0.
        mouseMove(control, control.swipe.leftItem.width, control.height / 2);
        compare(control.swipe.position, 1.0);
        tryCompare(control.background, "x", control.swipe.leftItem.width, 1000);
        mouseRelease(control, control.swipe.leftItem.width, control.height / 2, Qt.LeftButton);
    }

    // TODO: this somehow results in the behind item having a negative width
//    Component {
//        id: behindSwipeDelegateComponent
//        SwipeDelegate {
//            anchors.centerIn: parent
//            swipe.behind: Rectangle {
//                onXChanged: print("x changed", x)
//                anchors.left: {
//                    print("anchors.left expression", swipe.position)
//                    swipe.position < 0 ? parent.background.right : undefined
//                }
//                anchors.right: {
//                    print("anchors.right expression", swipe.position)
//                    swipe.position > 0 ? parent.background.left : undefined
//                }
//                width: parent.width
//                height: parent.height
//                color: "green"
//            }
//            swipe.left: null
//            swipe.right: null
//            Rectangle {
//                anchors.fill: parent
//                color: "transparent"
//                border.color: "darkorange"
//            }
//        }
//    }

    Component {
        id: behindSwipeDelegateComponent
        SwipeDelegate {
            text: "SwipeDelegate"
            width: 150
            anchors.centerIn: parent
            swipe.behind: Rectangle {
                x: swipe.position < 0 ? parent.background.x + parent.background.width
                    : (swipe.position > 0 ? parent.background.x - width : 0)
                width: parent.width
                height: parent.height
                color: "green"
            }
            swipe.left: null
            swipe.right: null
        }
    }

    function test_leadingTrailingBehindItem() {
        var control = createTemporaryObject(behindSwipeDelegateComponent, testCase);
        verify(control);

        swipe(control, 0.0, 1.0);
        verify(control.swipe.behindItem.visible);
        compare(control.swipe.behindItem.x, control.background.x - control.background.width);

        swipe(control, 1.0, -1.0);
        verify(control.swipe.behindItem.visible);
        compare(control.swipe.behindItem.x, control.background.x + control.background.width);

        swipe(control, -1.0, 1.0);
        verify(control.swipe.behindItem.visible);
        compare(control.swipe.behindItem.x, control.background.x - control.background.width);

        // Should be possible to "wrap" with a behind delegate specified.
        mousePress(control, control.width / 2, control.height / 2, Qt.LeftButton);
        mouseMove(control, control.width / 2 + control.swipe.behindItem.width * 0.8, control.height / 2);
        compare(control.swipe.position, -0.2);
        mouseRelease(control, control.width / 2 + control.swipe.behindItem.width * 0.8, control.height / 2, Qt.LeftButton);
        tryCompare(control.swipe, "position", 0.0);

        // Try wrapping the other way.
        swipe(control, 0.0, -1.0);
        verify(control.swipe.behindItem.visible);
        compare(control.swipe.behindItem.x, control.background.x + control.background.width);

        mousePress(control, control.width / 2, control.height / 2, Qt.LeftButton);
        mouseMove(control, control.width / 2 - control.swipe.behindItem.width * 0.8, control.height / 2);
        compare(control.swipe.position, 0.2);
        mouseRelease(control, control.width / 2 - control.swipe.behindItem.width * 0.8, control.height / 2, Qt.LeftButton);
        tryCompare(control.swipe, "position", 0.0);
    }

    Component {
        id: closeSwipeDelegateComponent

        SwipeDelegate {
            text: "SwipeDelegate"
            width: 150

            swipe.right: Item {
                width: parent.width
                height: parent.height

                SwipeDelegate.onClicked: swipe.close()
            }
        }
    }

    function test_close() {
        var control = createTemporaryObject(closeSwipeDelegateComponent, testCase);
        verify(control);

        var closedSpy = signalSpyComponent.createObject(control, { target: control.swipe, signalName: "closed" });
        verify(closedSpy);
        verify(closedSpy.valid);

        swipe(control, 0.0, -1.0);
        compare(control.swipe.rightItem.visible, true);
        // Should animate, so it shouldn't change right away.
        compare(control.swipe.rightItem.x, 0);
        tryCompare(control.swipe.rightItem, "x", control.background.x + control.background.width);

        mousePress(control);
        verify(control.swipe.rightItem.SwipeDelegate.pressed);

        mouseRelease(control);
        verify(!control.swipe.rightItem.SwipeDelegate.pressed);
        tryCompare(closedSpy, "count", 1);
        compare(control.swipe.position, 0);

        // Swiping after closing should work as normal.
        swipe(control, 0.0, -1.0);
    }

    function test_callCloseWhenAlreadyClosed() {
        let control = createTemporaryObject(swipeDelegateComponent, testCase)
        verify(control)

        let closedSpy = signalSpyComponent.createObject(control, { target: control.swipe, signalName: "closed" })
        verify(closedSpy)
        verify(closedSpy.valid)

        // Calling close() when it's already closed should have no effect.
        control.swipe.close()
        compare(closedSpy.count, 0)

        // The game goes for calling close() in response to a click.
        control.clicked.connect(function() { control.swipe.close() })
        mouseClick(control)
        compare(closedSpy.count, 0)
    }

    // Can't just connect to pressed in QML, because there is a pressed property
    // that conflicts with the signal.
    Component {
        id: swipeDelegateCloseOnPressedComponent

        SwipeDelegate {
            text: "SwipeDelegate"
            width: 150
            swipe.right: Rectangle {
                objectName: "rightItem"
                width: parent.width / 2
                height: parent.height
                color: "tomato"
            }

            onPressed: swipe.close()
        }
    }

    /*
        We don't want to support closing on pressed(); released() or clicked()
        should be used instead. However, calling swipe.close() in response to
        a press should still not cause closed() to be emitted.
    */
    function test_closeOnPressed() {
        let control = createTemporaryObject(swipeDelegateCloseOnPressedComponent, testCase)
        verify(control)

        swipe(control, 0.0, -1.0)

        let closedSpy = signalSpyComponent.createObject(control, { target: control.swipe, signalName: "closed" })
        verify(closedSpy)
        verify(closedSpy.valid)

        mousePress(control, control.width * 0.1)
        compare(closedSpy.count, 0)
        compare(control.swipe.position, -1.0)

        // Simulate a somewhat realistic delay between press and release
        // to ensure that the bug is triggered.
        wait(100)
        mouseRelease(control, control.width * 0.1)
        compare(closedSpy.count, 0)
        compare(control.swipe.position, -1.0)
    }

    Component {
        id: multiActionSwipeDelegateComponent

        SwipeDelegate {
            text: "SwipeDelegate"
            width: 150

            swipe.right: Item {
                objectName: "rightItemRoot"
                width: parent.width
                height: parent.height

                property alias firstAction: firstAction
                property alias secondAction: secondAction

                property int firstClickCount: 0
                property int secondClickCount: 0

                Row {
                    anchors.fill: parent
                    anchors.margins: 5

                    Rectangle {
                        id: firstAction
                        width: parent.width / 2
                        height: parent.height
                        color: "tomato"

                        SwipeDelegate.onClicked: ++firstClickCount
                    }
                    Rectangle {
                        id: secondAction
                        width: parent.width / 2
                        height: parent.height
                        color: "navajowhite"

                        SwipeDelegate.onClicked: ++secondClickCount
                    }
                }
            }
        }
    }

    // Tests that it's possible to have multiple non-interactive items in one delegate
    // (e.g. left/right/behind) that can each receive clicks.
    function test_multipleClickableActions() {
        var control = createTemporaryObject(multiActionSwipeDelegateComponent, testCase);
        verify(control);

        swipe(control, 0.0, -1.0);
        verify(control.swipe.rightItem);
        tryCompare(control.swipe, "complete", true);

        var firstClickedSpy = signalSpyComponent.createObject(control,
            { target: control.swipe.rightItem.firstAction.SwipeDelegate, signalName: "clicked" });
        verify(firstClickedSpy);
        verify(firstClickedSpy.valid);

        // Clicked within rightItem, but not within an item using the attached properties.
        mousePress(control, 2, 2);
        compare(control.swipe.rightItem.firstAction.SwipeDelegate.pressed, false);
        compare(firstClickedSpy.count, 0);

        mouseRelease(control, 2, 2);
        compare(control.swipe.rightItem.firstAction.SwipeDelegate.pressed, false);
        compare(firstClickedSpy.count, 0);

        // Click within the first item.
        mousePress(control.swipe.rightItem.firstAction, 0, 0);
        compare(control.swipe.rightItem.firstAction.SwipeDelegate.pressed, true);
        compare(firstClickedSpy.count, 0);

        mouseRelease(control.swipe.rightItem.firstAction, 0, 0);
        compare(control.swipe.rightItem.firstAction.SwipeDelegate.pressed, false);
        compare(firstClickedSpy.count, 1);
        compare(control.swipe.rightItem.firstClickCount, 1);

        var secondClickedSpy = signalSpyComponent.createObject(control,
            { target: control.swipe.rightItem.secondAction.SwipeDelegate, signalName: "clicked" });
        verify(secondClickedSpy);
        verify(secondClickedSpy.valid);

        // Click within the second item.
        mousePress(control.swipe.rightItem.secondAction, 0, 0);
        compare(control.swipe.rightItem.secondAction.SwipeDelegate.pressed, true);
        compare(secondClickedSpy.count, 0);

        mouseRelease(control.swipe.rightItem.secondAction, 0, 0);
        compare(control.swipe.rightItem.secondAction.SwipeDelegate.pressed, false);
        compare(secondClickedSpy.count, 1);
        compare(control.swipe.rightItem.secondClickCount, 1);
    }

    // Pressing on a "side action" and then dragging should eventually
    // cause the ListView to grab the mouse and start changing its contentY.
    // When this happens, it will grab the mouse and hence we must clear
    // that action's pressed state so that it doesn't stay pressed after releasing.
    function test_dragSideAction() {
        let listView = createTemporaryObject(removableDelegatesComponent, testCase);
        verify(listView);

        let control = listView.itemAt(0, 0);
        verify(control);

        // Expose the side action.
        swipe(control, 0.0, 1.0);
        verify(control.swipe.leftItem);
        tryCompare(control.swipe, "complete", true);

        let pressedSpy = signalSpyComponent.createObject(control,
            { target: control.swipe.leftItem.SwipeDelegate, signalName: "pressedChanged" });
        verify(pressedSpy);
        verify(pressedSpy.valid);

        let movingHorizontallySpy = createTemporaryObject(signalSpyComponent, testCase,
            { target: listView, signalName: "movingHorizontallyChanged" })
        verify(movingHorizontallySpy)
        verify(movingHorizontallySpy.valid)

        let movingVerticallySpy = createTemporaryObject(signalSpyComponent, testCase,
            { target: listView, signalName: "movingVerticallyChanged" })
        verify(movingVerticallySpy)
        verify(movingVerticallySpy.valid)

        let flickingHorizontallySpy = createTemporaryObject(signalSpyComponent, testCase,
            { target: listView, signalName: "flickingHorizontallyChanged" })
        verify(flickingHorizontallySpy)
        verify(flickingHorizontallySpy.valid)

        let flickingVerticallySpy = createTemporaryObject(signalSpyComponent, testCase,
            { target: listView, signalName: "flickingVerticallyChanged" })
        verify(flickingVerticallySpy)
        verify(flickingVerticallySpy.valid)

        // Drag the ListView vertically; its contentY should change.
        mouseDrag(listView, 20, 20, 0, listView.height);
        compare(pressedSpy.count, 2);

        // Wait for it to stop moving.
        tryCompare(listView, "flickingVertically", false)

        // 2 because it should change to true then false.
        compare(movingHorizontallySpy.count, 0)
        compare(movingVerticallySpy.count, 2)
        compare(flickingHorizontallySpy.count, 0)
        compare(flickingVerticallySpy.count, 2)
        compare(control.swipe.leftItem.SwipeDelegate.pressed, false);
    }

    // When the width of a SwipeDelegate changes (as it does upon portrait => landscape
    // rotation, for example), the positions of the contentItem and background items
    // should be updated accordingly.
    function test_contentItemPosOnWidthChanged() {
        var control = createTemporaryObject(swipeDelegateComponent, testCase);
        verify(control);

        swipe(control, 0.0, 1.0);

        var oldContentItemX = control.contentItem.x;
        var oldBackgroundX = control.background.x;
        control.width += 100;
        compare(control.contentItem.x, oldContentItemX + 100);
        compare(control.background.x, oldBackgroundX + 100);
    }

    function test_contentItemHeightOnHeightChanged() {
        var control = createTemporaryObject(swipeDelegateComponent, testCase);
        verify(control);

        // Try when swipe.complete is false.
        var originalHeight = control.height;
        var originalContentItemHeight = control.contentItem.height;
        verify(control.height !== 10);
        control.height = 10;
        compare(control.contentItem.height, control.availableHeight);
        verify(control.contentItem.height < originalContentItemHeight);
        compare(control.contentItem.y, control.topPadding);

        // Try when swipe.complete is true.
        control.height = originalHeight;
        swipe(control, 0.0, 1.0);
        control.height = 10;
        compare(control.contentItem.height, control.availableHeight);
        verify(control.contentItem.height < originalContentItemHeight);
        compare(control.contentItem.y, control.topPadding);
    }

    function test_releaseOutside_data() {
        return [
            { tag: "no delegates", component: emptySwipeDelegateComponent },
            { tag: "delegates", component: swipeDelegateComponent },
        ];
    }

    function test_releaseOutside(data) {
        var control = createTemporaryObject(data.component, testCase);
        verify(control);

        // Press and then release below the control.
        mouseSignalSequenceSpy.target = control;
        mouseSignalSequenceSpy.expectedSequence = [["pressedChanged", { "pressed": true }], "pressed", ["pressedChanged", { "pressed": false }]];
        mousePress(control, control.width / 2, control.height / 2, Qt.LeftButton);
        mouseMove(control, control.width / 2, control.height + 10);
        verify(mouseSignalSequenceSpy.success);

        mouseSignalSequenceSpy.expectedSequence = ["canceled"];
        mouseRelease(control, control.width / 2, control.height + 10, Qt.LeftButton);
        verify(mouseSignalSequenceSpy.success);

        // Press and then release to the right of the control.
        var hasDelegates = control.swipe.left || control.swipe.right || control.swipe.behind;
        mouseSignalSequenceSpy.target = control;
        mouseSignalSequenceSpy.expectedSequence = hasDelegates
            ? [["pressedChanged", { "pressed": true }], "pressed"]
            : [["pressedChanged", { "pressed": true }], "pressed", ["pressedChanged", { "pressed": false }]];
        mousePress(control, control.width / 2, control.height / 2, Qt.LeftButton);
        mouseMove(control, control.width + 10, control.height / 2);
        if (hasDelegates)
            verify(control.swipe.position > 0);
        verify(mouseSignalSequenceSpy.success);

        mouseSignalSequenceSpy.expectedSequence = hasDelegates ? [["pressedChanged", { "pressed": false }], "canceled"] : ["canceled"];
        mouseRelease(control, control.width + 10, control.height / 2, Qt.LeftButton);
        verify(mouseSignalSequenceSpy.success);
    }

    Component {
        id: leftRightWithLabelsComponent

        SwipeDelegate {
            id: delegate
            text: "SwipeDelegate"
            width: 150

            background.opacity: 0.5

            swipe.left: Rectangle {
                width: parent.width
                height: parent.height
                color: SwipeDelegate.pressed ? Qt.darker("green") : "green"

                property alias label: label

                Label {
                    id: label
                    text: "Left"
                    color: "white"
                    anchors.margins: 10
                    anchors.left: parent.left
                    anchors.verticalCenter: parent.verticalCenter
                }

                SwipeDelegate.onClicked: delegate.swipe.close()
            }

            swipe.right: Rectangle {
                width: parent.width
                height: parent.height
                anchors.right: parent.right
                color: SwipeDelegate.pressed ? Qt.darker("green") : "red"

                property alias label: label

                Label {
                    id: label
                    text: "Right"
                    color: "white"
                    anchors.margins: 10
                    anchors.right: parent.right
                    anchors.verticalCenter: parent.verticalCenter
                }

                SwipeDelegate.onClicked: delegate.swipe.close()
            }
        }
    }

    function test_beginSwipeOverRightItem() {
        var control = createTemporaryObject(leftRightWithLabelsComponent, testCase);
        verify(control);

        // Swipe to the left, exposing the right item.
        swipe(control, 0.0, -1.0);

        // Click to close it and go back to a position of 0.
        mouseClick(control);

        // TODO: Swipe to the left, with the mouse over the Label in the right item.
        // The left item should not become visible at any point.
        var rightLabel = control.swipe.rightItem.label;
        var overDragDistance = Math.round(dragDistance * 1.1);
        mousePress(rightLabel, rightLabel.width / 2, rightLabel.height / 2, Qt.rightButton);
        mouseMove(rightLabel, rightLabel.width / 2 - overDragDistance, rightLabel.height / 2);
        verify(!control.swipe.leftItem);

        mouseRelease(rightLabel, rightLabel.width / 2 - overDragDistance, control.height / 2, Qt.LeftButton);
        verify(!control.swipe.leftItem);
    }

    Component {
        id: swipeDelegateDisabledComponent

        SwipeDelegate {
            id: swipeDelegate
            text: "SwipeDelegate"
            width: parent.width
            height: checked ? implicitHeight * 2 : implicitHeight
            checkable: true

            swipe.enabled: false
            swipe.right: Label {
                text: swipeDelegate.checked ? qsTr("Expanded") : qsTr("Collapsed")
                width: parent.width
                height: parent.height
                padding: 12
                color: "white"
                verticalAlignment: Label.AlignVCenter
                horizontalAlignment: Label.AlignRight
            }
        }
    }

    function test_swipeEnabled() {
        var control = createTemporaryObject(swipeDelegateDisabledComponent, testCase);

        mousePress(control, control.width / 2, control.height / 2);
        verify(control.pressed);
        compare(control.swipe.position, 0.0);
        verify(!control.swipe.complete);
        verify(!control.swipe.leftItem);
        verify(!control.swipe.rightItem);

        // It shouldn't be possible to swipe.
        var overDragDistance = Math.round(dragDistance * 1.1);
        mouseMove(control, control.width / 2 - overDragDistance, control.height / 2);
        verify(control.pressed);
        compare(control.swipe.position, 0.0);
        verify(!control.swipe.complete);
        verify(!control.swipe.leftItem);
        verify(!control.swipe.rightItem);

        // Now move outside the right edge of the control and release.
        mouseMove(control, control.width * 1.1, control.height / 2);
        verify(control.pressed);
        compare(control.swipe.position, 0.0);
        verify(!control.swipe.complete);
        verify(!control.swipe.leftItem);
        verify(!control.swipe.rightItem);

        mouseRelease(control, control.width / 2, control.height / 2);
        verify(!control.pressed);
        compare(control.swipe.position, 0.0);
        verify(!control.swipe.complete);
        verify(!control.swipe.leftItem);
        verify(!control.swipe.rightItem);

        // Now enabled swiping so that we can swipe to the left.
        control.swipe.enabled = true;
        swipe(control, 0, -1);
        verify(control.swipe.complete);

        // Now that the swipe is complete, disable swiping and then try to swipe again.
        // It should stay at its position of -1.
        control.swipe.enabled = false;

        mousePress(control, control.width / 2, control.height / 2);
        verify(control.pressed);
        compare(control.swipe.position, -1.0);

        mouseMove(control, control.width / 2 + overDragDistance, control.height / 2);
        verify(control.pressed);
        compare(control.swipe.position, -1.0);
        verify(control.swipe.complete);

        mouseRelease(control, control.width / 2 + overDragDistance, control.height / 2);
        verify(!control.pressed);
        compare(control.swipe.position, -1.0);
        verify(control.swipe.complete);
    }

    function test_side() {
        compare(SwipeDelegate.Left, 1.0);
        compare(SwipeDelegate.Right, -1.0);
    }

    function test_open_side_data() {
        return [
            { tag: "left", side: SwipeDelegate.Left, position: 1, complete: true, left: greenLeftComponent, right: null, behind: null },
            { tag: "right", side: SwipeDelegate.Right, position: -1, complete: true, left: null, right: redRightComponent, behind: null },
            { tag: "behind,left", side: SwipeDelegate.Left, position: 1, complete: true, left: null, right: null, behind: greenLeftComponent },
            { tag: "behind,right", side: SwipeDelegate.Right, position: -1, complete: true, left: null, right: null, behind: redRightComponent },
            { tag: "left,behind", side: SwipeDelegate.Left, position: 1, complete: true, left: null, right: null, behind: greenLeftComponent },
            { tag: "right,behind", side: SwipeDelegate.Right, position: -1, complete: true, left: null, right: null, behind: redRightComponent },
            { tag: "left,null", side: SwipeDelegate.Left, position: 0, complete: false, left: null, right: null, behind: null },
            { tag: "right,null", side: SwipeDelegate.Right, position: 0, complete: false, left: null, right: null, behind: null },
            { tag: "invalid", side: 0, position: 0, complete: false, left: greenLeftComponent, right: null, behind: null }
        ]
    }

    function test_open_side(data) {
        var control = createTemporaryObject(emptySwipeDelegateComponent, testCase,
            {"swipe.left": data.left, "swipe.right": data.right, "swipe.behind": data.behind});
        verify(control);

        control.swipe.open(data.side);
        tryCompare(control.swipe, "position", data.position);
        tryCompare(control.swipe, "complete", data.complete);
    }

    Component {
        id: openSwipeDelegateComponent

        SwipeDelegate {
            text: "SwipeDelegate"
            width: 150

            onClicked: swipe.open(SwipeDelegate.Right)

            swipe.right: Item {
                width: parent.width
                height: parent.height
            }
        }
    }

    function test_open() {
        var control = createTemporaryObject(openSwipeDelegateComponent, testCase);
        verify(control);

        mouseClick(control);
        tryCompare(control.swipe, "position", SwipeDelegate.Right);
        tryCompare(control.background, "x", -control.background.width);

        // Swiping after opening should work as normal.
        swipe(control, SwipeDelegate.Right, 0.0);
        tryCompare(control.swipe, "position", 0.0);
        tryCompare(control.background, "x", 0);
    }

    Component {
        id: animationSwipeDelegateComponent

        SwipeDelegate {
            id: control
            text: "SwipeDelegate"
            width: 150
            swipe.left: greenLeftComponent
            swipe.right: redRightComponent
            swipe.transition: null

            property alias behavior: xBehavior
            property alias animation: numberAnimation

            background: Rectangle {
                color: control.down ? "#ccc" : "#fff"

                Behavior on x {
                    id: xBehavior
                    enabled: !control.down

                    NumberAnimation {
                        id: numberAnimation
                        easing.type: Easing.InOutCubic
                        duration: 400
                    }
                }
            }
        }
    }

    function test_animations() {
        // Test that animations are run when releasing from a drag.
        var control = createTemporaryObject(animationSwipeDelegateComponent, testCase);
        verify(control);

        mousePress(control, control.width / 2, control.height / 2, Qt.LeftButton);
        mouseMove(control, control.width - 1, control.height / 2);
        verify(control.down);
        verify(!control.behavior.enabled);
        verify(!control.animation.running);

        mouseRelease(control, control.width - 1, control.height / 2, Qt.LeftButton);
        verify(!control.down);
        verify(control.behavior.enabled);
        verify(control.animation.running);
    }

    function test_spacing() {
        var control = createTemporaryObject(swipeDelegateComponent, testCase, { text: "Some long, long, long text" })
        verify(control)
        verify(control.contentItem.implicitWidth + control.leftPadding + control.rightPadding > control.background.implicitWidth)

        var textLabel = findChild(control.contentItem, "label")
        verify(textLabel)

        // The implicitWidth of the IconLabel that all buttons use as their contentItem
        // should be equal to the implicitWidth of the Text while no icon is set.
        compare(control.contentItem.implicitWidth, textLabel.implicitWidth)

        // That means that spacing shouldn't affect it.
        control.spacing += 100
        compare(control.contentItem.implicitWidth, textLabel.implicitWidth)

        // The implicitWidth of the SwipeDelegate itself should, therefore, also never include spacing while no icon is set.
        compare(control.implicitWidth, textLabel.implicitWidth + control.leftPadding + control.rightPadding)
    }

    function test_display_data() {
        return [
            { "tag": "IconOnly", display: SwipeDelegate.IconOnly },
            { "tag": "TextOnly", display: SwipeDelegate.TextOnly },
            { "tag": "TextUnderIcon", display: SwipeDelegate.TextUnderIcon },
            { "tag": "TextBesideIcon", display: SwipeDelegate.TextBesideIcon },
            { "tag": "IconOnly, mirrored", display: SwipeDelegate.IconOnly, mirrored: true },
            { "tag": "TextOnly, mirrored", display: SwipeDelegate.TextOnly, mirrored: true },
            { "tag": "TextUnderIcon, mirrored", display: SwipeDelegate.TextUnderIcon, mirrored: true },
            { "tag": "TextBesideIcon, mirrored", display: SwipeDelegate.TextBesideIcon, mirrored: true }
        ]
    }

    function test_display(data) {
        var control = createTemporaryObject(swipeDelegateComponent, testCase, {
            text: "SwipeDelegate",
            display: data.display,
            width: 400,
            "icon.source": "qrc:/qt-project.org/imports/QtQuick/Controls.2/images/check.png",
            "LayoutMirroring.enabled": !!data.mirrored
        })
        verify(control)
        compare(control.icon.source, "qrc:/qt-project.org/imports/QtQuick/Controls.2/images/check.png")

        var iconImage = findChild(control.contentItem, "image")
        var textLabel = findChild(control.contentItem, "label")

        switch (control.display) {
        case SwipeDelegate.IconOnly:
            verify(iconImage)
            verify(!textLabel)
            compare(iconImage.x, (control.availableWidth - iconImage.width) / 2)
            compare(iconImage.y, (control.availableHeight - iconImage.height) / 2)
            break;
        case SwipeDelegate.TextOnly:
            verify(!iconImage)
            verify(textLabel)
            compare(textLabel.x, control.mirrored ? control.availableWidth - textLabel.width : 0)
            compare(textLabel.y, (control.availableHeight - textLabel.height) / 2)
            break;
        case SwipeDelegate.TextUnderIcon:
            verify(iconImage)
            verify(textLabel)
            compare(iconImage.x, (control.availableWidth - iconImage.width) / 2)
            compare(textLabel.x, (control.availableWidth - textLabel.width) / 2)
            verify(iconImage.y < textLabel.y)
            break;
        case SwipeDelegate.TextBesideIcon:
            verify(iconImage)
            verify(textLabel)
            if (control.mirrored)
                verify(textLabel.x < iconImage.x)
            else
                verify(iconImage.x < textLabel.x)
            compare(iconImage.y, (control.availableHeight - iconImage.height) / 2)
            compare(textLabel.y, (control.availableHeight - textLabel.height) / 2)
            break;
        }
    }

    function test_resizeParent() {
        let container = createTemporaryObject(itemComponent, testCase, { objectName: "container", width: 100, height: 200 })
        verify(container)

        let control = swipeDelegateComponent.createObject(container, { width: Qt.binding(function() { return container.width }) })
        verify(control)

        // Resize while closed.
        container.width = 200
        compare(container.width, 200)
        compare(control.width, 200)
        compare(control.background.width, 200)
        compare(control.contentItem.width, 200 - control.leftPadding - control.rightPadding)

        // Return to original size.
        container.width = 100
        compare(control.width, 100)
        compare(control.background.width, 100)
        compare(control.contentItem.width, 100 - control.leftPadding - control.rightPadding)

        // Swipe to the left to open.
        swipe(control, 0, -1.0)
        // Nothing should have changed except positions.
        compare(control.width, 100)
        compare(control.background.width, 100)
        compare(control.contentItem.width, 100 - control.leftPadding - control.rightPadding)

        // Resize while open.
        container.width = 200
        // The items should fill the width as usual.
        compare(control.width, 200)
        compare(control.background.width, 200)
        compare(control.contentItem.width, 200 - control.leftPadding - control.rightPadding)
    }
}
