aboutsummaryrefslogtreecommitdiffstats
path: root/tests/auto
diff options
context:
space:
mode:
Diffstat (limited to 'tests/auto')
-rw-r--r--tests/auto/quick/CMakeLists.txt1
-rw-r--r--tests/auto/quick/qquicknativedrag/CMakeLists.txt49
-rw-r--r--tests/auto/quick/qquicknativedrag/data/listViewDelegateDrag.qml41
-rw-r--r--tests/auto/quick/qquicknativedrag/tst_qquicknativedrag.cpp278
4 files changed, 369 insertions, 0 deletions
diff --git a/tests/auto/quick/CMakeLists.txt b/tests/auto/quick/CMakeLists.txt
index 035f960010..18ef46d300 100644
--- a/tests/auto/quick/CMakeLists.txt
+++ b/tests/auto/quick/CMakeLists.txt
@@ -43,6 +43,7 @@ if(QT_FEATURE_private_tests)
endif()
add_subdirectory(qquickdrag)
add_subdirectory(qquickdragattached)
+ add_subdirectory(qquicknativedrag)
add_subdirectory(qquickdroparea)
add_subdirectory(qquickdynamicpropertyanimation)
add_subdirectory(qquickflickable)
diff --git a/tests/auto/quick/qquicknativedrag/CMakeLists.txt b/tests/auto/quick/qquicknativedrag/CMakeLists.txt
new file mode 100644
index 0000000000..1f1006d02b
--- /dev/null
+++ b/tests/auto/quick/qquicknativedrag/CMakeLists.txt
@@ -0,0 +1,49 @@
+# Copyright (C) 2026 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+#####################################################################
+## tst_qquicknativedrag Test:
+#####################################################################
+
+if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT)
+ cmake_minimum_required(VERSION 3.16)
+ project(tst_qquicknativedrag LANGUAGES CXX)
+ find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST)
+endif()
+
+qt_internal_add_test(tst_qquicknativedrag
+ SOURCES
+ tst_qquicknativedrag.cpp
+ LIBRARIES
+ Qt::CorePrivate
+ Qt::Gui
+ Qt::GuiPrivate
+ Qt::Qml
+ Qt::QmlPrivate
+ Qt::Quick
+ Qt::QuickPrivate
+ Qt::QuickTestUtilsPrivate
+ TESTDATA ${test_data}
+)
+
+## Scopes:
+#####################################################################
+
+qt_internal_extend_target(tst_qquicknativedrag CONDITION ANDROID OR IOS
+ DEFINES
+ QT_QMLTEST_DATADIR=":/data"
+)
+
+qt_internal_extend_target(tst_qquicknativedrag CONDITION NOT ANDROID AND NOT IOS
+ DEFINES
+ QT_QMLTEST_DATADIR="${CMAKE_CURRENT_SOURCE_DIR}/data"
+)
+
+# Drag.dragType: Drag.Automatic goes through QDrag::exec(), which runs a nested event
+# loop that only an in-process QPlatformDrag lets synthetic input terminate. The
+# offscreen platform provides one, and implements QCursor::setPos(), which
+# QSimpleDrag::startDrag() needs to resolve the source window. The tests QSKIP
+# elsewhere, so running the binary directly on another platform is still safe.
+set_property(TEST tst_qquicknativedrag APPEND PROPERTY ENVIRONMENT
+ "QT_QPA_PLATFORM=offscreen"
+)
diff --git a/tests/auto/quick/qquicknativedrag/data/listViewDelegateDrag.qml b/tests/auto/quick/qquicknativedrag/data/listViewDelegateDrag.qml
new file mode 100644
index 0000000000..dffbd61c16
--- /dev/null
+++ b/tests/auto/quick/qquicknativedrag/data/listViewDelegateDrag.qml
@@ -0,0 +1,41 @@
+// Copyright (C) 2026 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+
+Rectangle {
+ width: 200
+ height: 200
+ color: "black"
+
+ // Receives the drag events that QSimpleDrag feeds back into this process.
+ DropArea {
+ objectName: "dropArea"
+ anchors.fill: parent
+ }
+
+ ListView {
+ id: list
+ objectName: "list"
+ anchors.fill: parent
+ model: 100
+ cacheBuffer: 0
+ boundsBehavior: Flickable.StopAtBounds
+ clip: true
+ // The view keeps currentItem alive outside visibleItems, so with the default
+ // currentIndex of 0 the first delegate would never be released when it scrolls
+ // out of view -- and the test needs it to be.
+ currentIndex: -1
+
+ delegate: Rectangle {
+ objectName: "delegate" + index
+ width: list.width
+ height: 50
+ color: "white"
+
+ Drag.dragType: Drag.Automatic
+ Drag.supportedActions: Qt.MoveAction
+ Drag.mimeData: { "text/plain": "" + index }
+ }
+ }
+}
diff --git a/tests/auto/quick/qquicknativedrag/tst_qquicknativedrag.cpp b/tests/auto/quick/qquicknativedrag/tst_qquicknativedrag.cpp
new file mode 100644
index 0000000000..4b5f1bee1a
--- /dev/null
+++ b/tests/auto/quick/qquicknativedrag/tst_qquicknativedrag.cpp
@@ -0,0 +1,278 @@
+// Copyright (C) 2026 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+// Exercises Drag.dragType: Drag.Automatic, i.e. the QDrag::exec() path. tst_qquickdrag
+// deliberately covers only Drag.Internal and Drag.None, because exec() runs a nested
+// event loop; this test drives that loop instead of avoiding it.
+
+#include <QtCore/qpointer.h>
+#include <QtCore/qregularexpression.h>
+#include <QtCore/qtimer.h>
+#include <QtGui/qcursor.h>
+#include <QtGui/qguiapplication.h>
+#include <QtGui/private/qguiapplication_p.h>
+#include <QtQml/qqmlcontext.h>
+#include <QtQml/qqmlexpression.h>
+#include <QtQuick/qquickitem.h>
+#include <QtQuick/qquickview.h>
+#include <QtQuick/private/qquickdrag_p.h>
+#include <QtTest/qsignalspy.h>
+#include <QtTest/qtest.h>
+
+#include <QtQuick/private/qquickitemview_p.h>
+
+#include <QtQuickTestUtils/private/qmlutils_p.h>
+#include <QtQuickTestUtils/private/viewtestutils_p.h>
+#include <QtQuickTestUtils/private/visualtestutils_p.h>
+
+#include <qpa/qplatformdrag.h>
+#include <qpa/qplatformintegration.h>
+#include <qpa/qwindowsysteminterface.h>
+
+#include <functional>
+
+using namespace Qt::StringLiterals;
+using namespace std::chrono_literals;
+
+// QDrag::exec() runs a nested event loop that, on most platforms, only a real user can
+// terminate. The offscreen platform installs the in-process QSimpleDrag, which drives
+// itself from ordinary mouse and key events, and implements QCursor::setPos(), which
+// QSimpleDrag::startDrag() needs to resolve the source window. The CMakeLists.txt for
+// this test selects it; "minimal" is not a substitute, as it has no QPlatformCursor.
+static bool haveInProcessDrag()
+{
+ if (QGuiApplication::platformName() != "offscreen"_L1)
+ return false;
+ // QBasicDrag, which QSimpleDrag derives from, is a QObject: it installs itself as an
+ // application event filter to track the mouse. A stub implementation that returns
+ // Qt::IgnoreAction without running an event loop is not, so this also skips rather
+ // than fails when built against a Qt version whose offscreen plugin still has one.
+ return dynamic_cast<QObject *>(QGuiApplicationPrivate::platformIntegration()->drag());
+}
+
+#define SKIP_IF_NO_IN_PROCESS_DRAG() \
+ do { \
+ if (!haveInProcessDrag()) \
+ QSKIP("Needs the in-process QSimpleDrag; run with -platform offscreen"); \
+ } while (false)
+
+// Input is posted rather than delivered with QTest::mouse*(), which is not usable from
+// inside a drag: those go through qt_handleMouseEvent(), which delivers synchronously
+// and then calls qApp->processEvents(). That nested processEvents() clears the event
+// dispatcher's interrupt flag and drains its wakeup pipe, so if the event ended the
+// drag, the QEventLoop::exit() it triggered is swallowed and the drag loop blocks until
+// something else happens to wake it. Posting lets the drag's own loop drain the queue.
+static void postMouseMove(QWindow *window, const QPoint &local)
+{
+ QWindowSystemInterface::handleMouseEvent(window, local, window->mapToGlobal(local),
+ Qt::LeftButton, Qt::NoButton, QEvent::MouseMove);
+}
+
+static void postMouseRelease(QWindow *window, const QPoint &local)
+{
+ QWindowSystemInterface::handleMouseEvent(window, local, window->mapToGlobal(local),
+ Qt::NoButton, Qt::LeftButton,
+ QEvent::MouseButtonRelease);
+}
+
+class tst_QQuickNativeDrag : public QQmlDataTest
+{
+ Q_OBJECT
+public:
+ tst_QQuickNativeDrag() : QQmlDataTest(QT_QMLTEST_DATADIR) {}
+
+private slots:
+ void init() override;
+
+ void completes();
+ void delegateDestroyedWhileDragging_data();
+ void delegateDestroyedWhileDragging();
+
+private:
+ // Starts a native drag on item, invoking insideLoop from within QDrag::exec()'s
+ // nested event loop.
+ static void runDrag(QQuickView *window, QQuickItem *item, QQuickDragAttached *attached,
+ std::function<void()> insideLoop);
+ static QQuickDragAttached *dragAttached(QQuickItem *item)
+ {
+ return qobject_cast<QQuickDragAttached *>(
+ qmlAttachedPropertiesObject<QQuickDrag>(item, /*create*/ true));
+ }
+ static void evaluate(QObject *scope, const QString &expression)
+ {
+ QQmlExpression expr(qmlContext(scope), scope, expression);
+ const QVariant result = expr.evaluate();
+ Q_UNUSED(result);
+ QVERIFY2(!expr.hasError(), qPrintable(expr.error().toString()));
+ }
+};
+
+void tst_QQuickNativeDrag::init()
+{
+ QQmlDataTest::init();
+ // runDrag() warns when its watchdog has to break into a stuck nested event loop.
+ QTest::failOnWarning(QRegularExpression(u"^watchdog:"_s));
+}
+
+void tst_QQuickNativeDrag::runDrag(QQuickView *window, QQuickItem *item,
+ QQuickDragAttached *attached,
+ std::function<void()> insideLoop)
+{
+ const QPoint pressPos = item->mapToScene(QPointF(10, 10)).toPoint();
+
+ // QSimpleDrag::startDrag() resolves the source window from QCursor::pos() rather
+ // than from the last mouse event, so this is needed before the drag starts. On
+ // offscreen it takes effect immediately, with no round trip to a compositor.
+ QCursor::setPos(window->mapToGlobal(pressPos));
+ QCoreApplication::processEvents();
+ QCOMPARE(QCursor::pos(), window->mapToGlobal(pressPos));
+
+ postMouseMove(window, pressPos);
+ QWindowSystemInterface::handleMouseEvent(window, pressPos, window->mapToGlobal(pressPos),
+ Qt::LeftButton, Qt::LeftButton,
+ QEvent::MouseButtonPress);
+ QCoreApplication::processEvents();
+
+ // dragStarted() is emitted from QQuickDragAttachedPrivate::startDrag() just before
+ // QDrag::exec(), so a queued call from it is delivered inside the nested loop --
+ // by which time QBasicDrag has installed the event filter that makes posted input
+ // drive the drag.
+ if (insideLoop) {
+ QObject::connect(attached, &QQuickDragAttached::dragStarted, attached,
+ std::move(insideLoop), Qt::QueuedConnection);
+ }
+
+ // Fail rather than hang if nothing terminates the loop.
+ QTimer watchdog;
+ watchdog.setSingleShot(true);
+ watchdog.setInterval(5s);
+ QObject::connect(&watchdog, &QTimer::timeout, &watchdog, [] {
+ qWarning("watchdog: nested drag event loop did not terminate");
+ if (QPlatformDrag *pd = QGuiApplicationPrivate::platformIntegration()->drag())
+ pd->cancelDrag();
+ });
+ watchdog.start();
+
+ QPointer<QQuickDragAttached> guard(attached);
+ evaluate(item, u"Drag.active = true"_s);
+ // setActive() defers startDrag() to the event loop, and exec() then blocks until the
+ // input posted by insideLoop ends the drag -- so spinning here runs the whole thing.
+ // The attached object may not survive it, hence the guard.
+ QTRY_VERIFY(!guard || !guard->isActive());
+}
+
+// Baseline: a native drag from a delegate starts, is seen by the DropArea, and finishes.
+void tst_QQuickNativeDrag::completes()
+{
+ SKIP_IF_NO_IN_PROCESS_DRAG();
+
+ QQuickView window;
+ QVERIFY(QQuickTest::showView(window, testFileUrl(u"listViewDelegateDrag.qml"_s)));
+ QQuickItemView *view = window.rootObject()->findChild<QQuickItemView *>();
+ QVERIFY(view);
+ QQuickItem *delegate = QQuickVisualTestUtils::findViewDelegateItem(view, 0);
+ QVERIFY(delegate);
+ QQuickDragAttached *attached = dragAttached(delegate);
+ QVERIFY(attached);
+ const QQuickItem *dropArea = window.rootObject()->findChild<QQuickItem *>(u"dropArea"_s);
+ QVERIFY(dropArea);
+
+ QSignalSpy startedSpy(attached, SIGNAL(dragStarted()));
+ QSignalSpy finishedSpy(attached, SIGNAL(dragFinished(Qt::DropAction)));
+
+ const QPoint dropPos = delegate->mapToScene(QPointF(10, 120)).toPoint();
+ runDrag(&window, delegate, attached, [&] {
+ postMouseMove(&window, dropPos);
+ postMouseRelease(&window, dropPos);
+ });
+
+ QCOMPARE(startedSpy.size(), 1);
+ QTRY_COMPARE(finishedSpy.size(), 1);
+ QVERIFY(!attached->isActive());
+ QVERIFY(!attached->target());
+}
+
+void tst_QQuickNativeDrag::delegateDestroyedWhileDragging_data()
+{
+ QTest::addColumn<QString>("destroyExpression");
+ // The delegate scrolls out of the visible area, and with cacheBuffer 0 the view
+ // releases it immediately. This is what QTBUG-124663 does by auto-scrolling.
+ QTest::newRow("recycled") << u"list.contentY = 50 * 40"_s;
+ // The model rows backing every delegate go away. Nothing item-view-specific is
+ // needed to reproduce this -- any delegate model teardown will do.
+ QTest::newRow("model reset") << u"list.model = 0"_s;
+}
+
+// QTBUG-124663: the delegate that owns the QQuickDragAttached -- and that the QDrag is
+// parented to -- is destroyed inside QDrag::exec()'s nested event loop. Must not crash,
+// must not leave the drag active, and must not leave the DropArea entered.
+void tst_QQuickNativeDrag::delegateDestroyedWhileDragging()
+{
+ SKIP_IF_NO_IN_PROCESS_DRAG();
+ QFETCH(const QString, destroyExpression);
+
+ QQuickView window;
+ QVERIFY(QQuickTest::showView(window, testFileUrl(u"listViewDelegateDrag.qml"_s)));
+ QQuickItemView *view = window.rootObject()->findChild<QQuickItemView *>();
+ QVERIFY(view);
+ QQuickItem *delegate = QQuickVisualTestUtils::findViewDelegateItem(view, 0);
+ QVERIFY(delegate);
+ QPointer<QQuickItem> delegateGuard(delegate);
+ QPointer<QQuickDragAttached> attached(dragAttached(delegate));
+ QVERIFY(attached);
+ const QQuickItem *dropArea = window.rootObject()->findChild<QQuickItem *>(u"dropArea"_s);
+ QVERIFY(dropArea);
+
+ QSignalSpy startedSpy(attached, SIGNAL(dragStarted()));
+ QSignalSpy finishedSpy(attached, SIGNAL(dragFinished(Qt::DropAction)));
+ QSignalSpy enteredSpy(dropArea, SIGNAL(entered(QQuickDragEvent*)));
+ QSignalSpy exitedSpy(dropArea, SIGNAL(exited()));
+
+ QObject *root = window.rootObject();
+ runDrag(&window, delegate, attached, [&] {
+ // We are inside QDrag::exec() here.
+ evaluate(root, destroyExpression);
+ // The view releases the delegate on its next polish, and the delegate model then
+ // queues a deferred delete. Let both happen, still inside the nested loop, which
+ // is what the stack trace in the bug report shows. Stop as soon as the delegate
+ // is gone: processEvents() after the drag has ended would clear the event
+ // dispatcher interrupt that unwinds exec().
+ for (int i = 0; i < 10 && !delegateGuard.isNull(); ++i) {
+ QCoreApplication::processEvents();
+ QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
+ }
+ });
+
+ QVERIFY(delegateGuard.isNull());
+ QCOMPARE(startedSpy.size(), 1);
+
+ // The attached object is a QObject child of the delegate, so it is gone too -- but
+ // it must have reported the drag as finished on the way out, and the DropArea must
+ // not be left thinking a drag is still in progress.
+ QCOMPARE(finishedSpy.size(), 1);
+ QTRY_COMPARE(exitedSpy.size(), enteredSpy.size());
+
+ // The reported symptom is that the drag is "internally still active" afterwards.
+ // Prove the state machine recovered by running a second, uneventful drag. Undo
+ // whatever this data row did first, so that there is a delegate to drag again.
+ evaluate(root, u"list.model = 100"_s);
+ evaluate(root, u"list.contentY = 0"_s);
+ QQuickItem *next = nullptr;
+ QTRY_VERIFY((next = QQuickVisualTestUtils::findViewDelegateItem(view, 1)));
+ QQuickDragAttached *nextAttached = dragAttached(next);
+ QVERIFY(nextAttached);
+ QSignalSpy secondFinished(nextAttached, SIGNAL(dragFinished(Qt::DropAction)));
+
+ const QPoint dropPos = next->mapToScene(QPointF(10, 30)).toPoint();
+ runDrag(&window, next, nextAttached, [&] {
+ postMouseMove(&window, dropPos);
+ postMouseRelease(&window, dropPos);
+ });
+
+ QTRY_COMPARE(secondFinished.size(), 1);
+ QVERIFY(!nextAttached->isActive());
+}
+
+QTEST_MAIN(tst_QQuickNativeDrag)
+
+#include "tst_qquicknativedrag.moc"