aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/auto/qml/CMakeLists.txt1
-rw-r--r--tests/auto/qml/qqmlrangemodel/CMakeLists.txt38
-rw-r--r--tests/auto/qml/qqmlrangemodel/data/gadgetList.qml35
-rw-r--r--tests/auto/qml/qqmlrangemodel/data/gadgetRange.qml35
-rw-r--r--tests/auto/qml/qqmlrangemodel/data/gadgetTable.qml60
-rw-r--r--tests/auto/qml/qqmlrangemodel/data/intRange.qml26
-rw-r--r--tests/auto/qml/qqmlrangemodel/data/objectList.qml42
-rw-r--r--tests/auto/qml/qqmlrangemodel/data/objectRange.qml36
-rw-r--r--tests/auto/qml/qqmlrangemodel/data/variantList.qml24
-rw-r--r--tests/auto/qml/qqmlrangemodel/tst_qqmlrangemodel.cpp552
10 files changed, 849 insertions, 0 deletions
diff --git a/tests/auto/qml/CMakeLists.txt b/tests/auto/qml/CMakeLists.txt
index 93455d5b6e..ad2416a17a 100644
--- a/tests/auto/qml/CMakeLists.txt
+++ b/tests/auto/qml/CMakeLists.txt
@@ -149,6 +149,7 @@ if(QT_FEATURE_private_tests)
add_subdirectory(qqmltranslation)
add_subdirectory(qqmlimport)
add_subdirectory(qqmlobjectmodel)
+ add_subdirectory(qqmlrangemodel)
add_subdirectory(qqmltablemodel)
add_subdirectory(qqmlsortfilterproxymodel)
add_subdirectory(qqmltreemodel)
diff --git a/tests/auto/qml/qqmlrangemodel/CMakeLists.txt b/tests/auto/qml/qqmlrangemodel/CMakeLists.txt
new file mode 100644
index 0000000000..6266da7953
--- /dev/null
+++ b/tests/auto/qml/qqmlrangemodel/CMakeLists.txt
@@ -0,0 +1,38 @@
+# Copyright (C) 2025 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT)
+ cmake_minimum_required(VERSION 3.16)
+ project(tst_qqmlrangemodel LANGUAGES CXX)
+ find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST)
+endif()
+
+# Collect test data
+file(GLOB_RECURSE test_data_glob
+ RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}
+ data/*)
+list(APPEND test_data ${test_data_glob})
+
+qt_internal_add_test(tst_qqmlrangemodel
+ SOURCES
+ tst_qqmlrangemodel.cpp
+ LIBRARIES
+ Qt::Gui
+ Qt::Qml
+ Qt::QmlPrivate
+ Qt::QmlModelsPrivate
+ Qt::Quick
+ Qt::QuickPrivate
+ Qt::QuickTestUtilsPrivate
+ TESTDATA ${test_data}
+)
+
+qt_internal_extend_target(tst_qqmlrangemodel CONDITION ANDROID OR IOS
+ DEFINES
+ QT_QMLTEST_DATADIR=":/data"
+)
+
+qt_internal_extend_target(tst_qqmlrangemodel CONDITION NOT ANDROID AND NOT IOS
+ DEFINES
+ QT_QMLTEST_DATADIR="${CMAKE_CURRENT_SOURCE_DIR}/data"
+)
diff --git a/tests/auto/qml/qqmlrangemodel/data/gadgetList.qml b/tests/auto/qml/qqmlrangemodel/data/gadgetList.qml
new file mode 100644
index 0000000000..a6821a85ba
--- /dev/null
+++ b/tests/auto/qml/qqmlrangemodel/data/gadgetList.qml
@@ -0,0 +1,35 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+
+ListView {
+ id: listView
+ width: 200
+ height: 320
+ required model
+
+ delegate: Rectangle {
+ width: listView.width;
+ height: 25
+
+ required property var modelData
+ required property var text
+ property int number: modelData.number
+
+ Text {
+ anchors.fill: parent
+ text: parent.text
+ }
+
+ function setValue(value)
+ {
+ modelData.text = value;
+ }
+
+ function setModelData(value)
+ {
+ modelData = value;
+ }
+ }
+}
diff --git a/tests/auto/qml/qqmlrangemodel/data/gadgetRange.qml b/tests/auto/qml/qqmlrangemodel/data/gadgetRange.qml
new file mode 100644
index 0000000000..84769f1e77
--- /dev/null
+++ b/tests/auto/qml/qqmlrangemodel/data/gadgetRange.qml
@@ -0,0 +1,35 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+
+ListView {
+ id: listView
+ width: 200
+ height: 320
+ required model
+
+ delegate: Rectangle {
+ width: listView.width;
+ height: 25
+
+ required property var modelData
+ required property string text
+ property int number: modelData.number
+
+ Text {
+ anchors.fill: parent
+ text: parent.text
+ }
+
+ function setValue(value)
+ {
+ text = value;
+ }
+
+ function setModelData(value)
+ {
+ modelData = value;
+ }
+ }
+}
diff --git a/tests/auto/qml/qqmlrangemodel/data/gadgetTable.qml b/tests/auto/qml/qqmlrangemodel/data/gadgetTable.qml
new file mode 100644
index 0000000000..be313d90ec
--- /dev/null
+++ b/tests/auto/qml/qqmlrangemodel/data/gadgetTable.qml
@@ -0,0 +1,60 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+
+TableView {
+ id: tableView
+ width: 250
+ height: 320
+ columnSpacing: 10
+ rowSpacing: 10
+
+ required delegateModelAccess
+ required model
+ property var currentItem
+
+ selectionModel: ItemSelectionModel {
+ }
+
+ Component.onCompleted: {
+ selectionModel.setCurrentIndex(model.index(0, 0), ItemSelectionModel.SelectCurrent)
+ }
+
+ delegate: Rectangle {
+ id: cell
+ implicitWidth: 100
+ implicitHeight: 25
+
+ required property bool current
+ Binding {
+ when: cell.current
+ tableView.currentItem: cell
+ }
+
+ required property var modelData
+ required property string text
+ property int number: modelData.number
+
+
+ Text {
+ anchors.fill: parent
+ text: cell.text + ": " + cell.number
+ }
+
+ function setValue(value: string)
+ {
+ text = value;
+ }
+
+ function setModelData(value)
+ {
+ modelData = value;
+ }
+
+ function setModelDataNumber(number: int)
+ {
+ modelData.number = number;
+ }
+ }
+}
diff --git a/tests/auto/qml/qqmlrangemodel/data/intRange.qml b/tests/auto/qml/qqmlrangemodel/data/intRange.qml
new file mode 100644
index 0000000000..e7efb1e8f2
--- /dev/null
+++ b/tests/auto/qml/qqmlrangemodel/data/intRange.qml
@@ -0,0 +1,26 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+
+ListView {
+ id: listView
+ width: 200
+ height: 320
+
+ required delegateModelAccess
+ required model
+
+ delegate: Rectangle {
+ width: listView.width;
+ height: 25
+
+ required property var modelData
+ property int currentValue: modelData
+
+ function setValue(value: int)
+ {
+ modelData = value
+ }
+ }
+}
diff --git a/tests/auto/qml/qqmlrangemodel/data/objectList.qml b/tests/auto/qml/qqmlrangemodel/data/objectList.qml
new file mode 100644
index 0000000000..fa45b3d9b4
--- /dev/null
+++ b/tests/auto/qml/qqmlrangemodel/data/objectList.qml
@@ -0,0 +1,42 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+
+ListView {
+ id: listView
+ width: 200
+ height: 320
+ required model
+
+ delegate: Rectangle {
+ width: listView.width;
+ height: 25
+
+ required property var modelData
+ required property int number
+ required property string text
+
+ property var currentValue: number + ": " + text
+ property var currentData: modelData.number + ": " + modelData.text
+
+ Text {
+ text: currentValue
+ }
+
+ function setValue(value: int)
+ {
+ number = value
+ }
+
+ function setModelValue(value: int)
+ {
+ modelData.number = value;
+ }
+
+ function setModelData(data)
+ {
+ modelData = data;
+ }
+ }
+}
diff --git a/tests/auto/qml/qqmlrangemodel/data/objectRange.qml b/tests/auto/qml/qqmlrangemodel/data/objectRange.qml
new file mode 100644
index 0000000000..47e83cb7eb
--- /dev/null
+++ b/tests/auto/qml/qqmlrangemodel/data/objectRange.qml
@@ -0,0 +1,36 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+
+ListView {
+ id: listView
+ width: 200
+ height: 320
+
+ required delegateModelAccess
+ required model
+
+ delegate: Rectangle {
+ width: listView.width;
+ height: 25
+
+ required property var modelData
+ required property int number
+ property int modelNumber: modelData.number
+
+ Text {
+ text: number
+ }
+
+ function setValue(value)
+ {
+ number = value
+ }
+
+ function setModelValue(value: int)
+ {
+ modelData.number = value
+ }
+ }
+}
diff --git a/tests/auto/qml/qqmlrangemodel/data/variantList.qml b/tests/auto/qml/qqmlrangemodel/data/variantList.qml
new file mode 100644
index 0000000000..204b265783
--- /dev/null
+++ b/tests/auto/qml/qqmlrangemodel/data/variantList.qml
@@ -0,0 +1,24 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+
+ListView {
+ id: listView
+ width: 200
+ height: 320
+ required model
+
+ delegate: Rectangle {
+ width: listView.width;
+ height: 25
+
+ required property var modelData
+ property var currentValue: modelData
+
+ function setValue(value: int)
+ {
+ modelData = value;
+ }
+ }
+}
diff --git a/tests/auto/qml/qqmlrangemodel/tst_qqmlrangemodel.cpp b/tests/auto/qml/qqmlrangemodel/tst_qqmlrangemodel.cpp
new file mode 100644
index 0000000000..443625a50f
--- /dev/null
+++ b/tests/auto/qml/qqmlrangemodel/tst_qqmlrangemodel.cpp
@@ -0,0 +1,552 @@
+// Copyright (C) 2025 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include <QtTest/qtest.h>
+#include <QtTest/qsignalspy.h>
+#include <QtCore/qhash.h>
+#include <QtCore/qitemselectionmodel.h>
+#include <QtCore/qrangemodel.h>
+#include <QtQmlModels/private/qqmldelegatemodel_p.h>
+#include <QtQuick/qquickview.h>
+#include <QtQuickTestUtils/private/qmlutils_p.h>
+#include <QtQuickTestUtils/private/viewtestutils_p.h>
+
+using namespace Qt::StringLiterals;
+
+class tst_QQmlRangeModel : public QQmlDataTest
+{
+ Q_OBJECT
+
+public:
+ tst_QQmlRangeModel()
+ : QQmlDataTest(QT_QMLTEST_DATADIR)
+ {}
+
+private:
+ using RoleNames = QHash<int, QByteArray>;
+
+ std::unique_ptr<QQuickView> makeView(const QVariantMap &properties) const;
+
+ void listTest_data();
+ void rangeModelTest_data();
+
+ // subclass of QRangeModel allowing us to monitor API traffic
+ struct RangeModel : QRangeModel
+ {
+ template <typename Data>
+ RangeModel(Data &&data)
+ : QRangeModel(std::forward<Data>(data))
+ {}
+
+ mutable QList<int> dataCalls;
+ QList<int> setDataCalls;
+
+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
+ {
+ dataCalls << role;
+ return QRangeModel::data(index, role);
+ }
+
+ bool setData(const QModelIndex &index, const QVariant &data, int role = Qt::EditRole) override
+ {
+ setDataCalls << role;
+ return QRangeModel::setData(index, data, role);
+ }
+ };
+
+private slots:
+ // reference cases using QList<...> as models
+ void variantList_data() { listTest_data(); }
+ void variantList();
+ void objectList_data() { listTest_data(); }
+ void objectList();
+ void gadgetList_data() { listTest_data(); }
+ void gadgetList();
+
+ // QRangeModel tests
+ void intRange_data() { rangeModelTest_data(); }
+ void intRange();
+ void objectRange_data() { rangeModelTest_data(); }
+ void objectRange();
+ void gadgetRange_data() { rangeModelTest_data(); }
+ void gadgetRange();
+
+ void gadgetTable_data() { rangeModelTest_data(); }
+ void gadgetTable();
+};
+
+class Entry : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(int number READ number WRITE setNumber NOTIFY numberChanged)
+ Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged)
+public:
+ enum EntryRoles {
+ NumberRole = Qt::UserRole,
+ TextRole,
+ };
+ Entry(int number, const QString &text)
+ : m_number(number), m_text(text)
+ {}
+
+ int number() const { return m_number; }
+ void setNumber(int number)
+ {
+ if (m_number == number)
+ return;
+ m_number = number;
+ emit numberChanged();
+ }
+
+ QString text() const { return m_text; }
+ void setText(const QString &text)
+ {
+ if (m_text == text)
+ return;
+ m_text = text;
+ emit textChanged();
+ }
+
+ QString toString() const
+ {
+ return u"%1: %2"_s.arg(m_number).arg(m_text);
+ }
+
+signals:
+ void numberChanged();
+ void textChanged();
+
+private:
+ int m_number;
+ QString m_text;
+};
+
+template <>
+struct QRangeModel::RowOptions<Entry>
+{
+ static constexpr auto rowCategory = RowCategory::MultiRoleItem;
+};
+
+class Gadget
+{
+ Q_GADGET
+ Q_PROPERTY(int number READ number WRITE setNumber)
+ Q_PROPERTY(QString text READ text WRITE setText)
+ QML_VALUE_TYPE(gadget)
+
+public:
+ enum GadgetRoles {
+ NumberRole = Qt::UserRole,
+ TextRole,
+ };
+
+ Gadget() : m_number(-1) {}
+
+ Gadget(int number, const QString &text)
+ : m_number(number), m_text(text)
+ {}
+
+ int number() const { return m_number; }
+ void setNumber(int number) { m_number = number; }
+
+ QString text() const { return m_text; }
+ void setText(const QString &text) { m_text = text; }
+
+private:
+ friend bool operator==(const Gadget &lhs, const Gadget &rhs)
+ {
+ return lhs.m_number == rhs.m_number
+ && lhs.m_text == rhs.m_text;
+ }
+ int m_number;
+ QString m_text;
+};
+
+template <>
+struct QRangeModel::RowOptions<Gadget>
+{
+ static constexpr auto rowCategory = RowCategory::MultiRoleItem;
+};
+
+
+std::unique_ptr<QQuickView> tst_QQmlRangeModel::makeView(const QVariantMap &properties) const
+{
+ auto view = std::make_unique<QQuickView>();
+ view->setInitialProperties(properties);
+
+ const QString testFunction = QString::fromUtf8(QTest::currentTestFunction());
+ if (!QQuickTest::showView(*view, testFileUrl(testFunction + ".qml")))
+ return {};
+ return view;
+}
+
+// The first two tests are for reference, documenting how modelData works with
+// lists as models.
+void tst_QQmlRangeModel::listTest_data()
+{
+ QTest::addColumn<QQmlDelegateModel::DelegateModelAccess>("delegateModelAccess");
+ QTest::addColumn<bool>("writeBack");
+
+ QTest::addRow("ReadOnly") << QQmlDelegateModel::ReadOnly << false;
+ QTest::addRow("ReadWrite") << QQmlDelegateModel::ReadWrite << true;
+}
+
+void tst_QQmlRangeModel::variantList()
+{
+ QFETCH(const QQmlDelegateModel::DelegateModelAccess, delegateModelAccess);
+
+ QVariantList numbers = {1};
+ auto view = makeView({
+ {"delegateModelAccess", delegateModelAccess},
+ {"model", QVariant::fromValue(numbers)}
+ });
+ QVERIFY(view);
+
+ QObject *currentItem = nullptr;
+ QTRY_VERIFY(currentItem = view->rootObject()->property("currentItem").value<QObject *>());
+ QSignalSpy currentValueSpy(currentItem, SIGNAL(currentValueChanged()));
+ auto currentValue = currentItem->property("currentValue");
+ QCOMPARE(currentValue, numbers.at(0));
+
+ QMetaObject::invokeMethod(currentItem, "setValue", 42);
+ QCOMPARE(currentValueSpy.count(), 1);
+ QCOMPARE(currentItem->property("currentValue"), 42);
+
+ // the view changing the model cannot modify the C++ data, not even
+ // with ReadWrite model access, as we pass a copy of QVariantList.
+ QCOMPARE(numbers, QVariantList{1});
+}
+
+void tst_QQmlRangeModel::objectList()
+{
+ QFETCH(const QQmlDelegateModel::DelegateModelAccess, delegateModelAccess);
+ QFETCH(const bool, writeBack);
+
+ QPointer<Entry> entry = new Entry(1, "one");
+ QList<Entry *> objects = {
+ entry.data(),
+ };
+ auto cleanup = qScopeGuard([&objects]{ qDeleteAll(objects); });
+
+ auto view = makeView({
+ {"delegateModelAccess", delegateModelAccess},
+ {"model", QVariant::fromValue(objects)}
+ });
+ QVERIFY(view);
+
+ QObject *currentItem = nullptr;
+ QTRY_VERIFY(currentItem = view->rootObject()->property("currentItem").value<QObject *>());
+ QSignalSpy currentValueSpy(currentItem, SIGNAL(currentValueChanged()));
+ QSignalSpy currentDataSpy(currentItem, SIGNAL(currentDataChanged()));
+ auto currentValue = currentItem->property("currentValue");
+
+ QCOMPARE(currentValue, objects.at(0)->toString());
+
+ // changing the required properties from QML...
+ QMetaObject::invokeMethod(currentItem, "setValue", 42);
+ QCOMPARE(currentValueSpy.count(), 1);
+
+ // ... changes modelData and C++ side only in ReadWrite access mode
+ QCOMPARE(currentDataSpy.count(), writeBack ? 1 : 0);
+ QCOMPARE(currentItem->property("currentValue"), "42: one");
+ QCOMPARE(currentItem->property("currentData"), writeBack ? "42: one" : "1: one");
+ QCOMPARE(entry->number(), writeBack ? 42 : 1);
+
+ // changing C++ doesn't update required property values in either case
+ entry->setText("fortytwo");
+ QCOMPARE(currentValueSpy.count(), 1);
+ QCOMPARE(currentItem->property("currentValue"), "42: one");
+
+ // but does update modelData
+ QCOMPARE(currentDataSpy.count(), writeBack ? 2 : 1);
+ QCOMPARE(currentItem->property("currentData"), entry->toString());
+
+ // replacing modelData triggers refresh of required properties, but also
+ // messes things up a bit
+ auto newEntry = std::make_unique<Entry>(2, "two");
+ QTest::ignoreMessage(QtWarningMsg, QRegularExpression("TypeError: Cannot read property '.*' of null"));
+ QMetaObject::invokeMethod(currentItem, "setModelData", QVariant::fromValue(newEntry.get()));
+ QVERIFY(entry); // old object still alive
+ QCOMPARE(currentItem->property("currentValue"), writeBack ? "42: fortytwo" : "42: one");
+ QCOMPARE(entry->toString(), writeBack ? "42: fortytwo" : "1: fortytwo");
+ QCOMPARE(currentItem->property("currentData"), "2: two");
+ QCOMPARE(newEntry->toString(), "2: two");
+}
+
+void tst_QQmlRangeModel::gadgetList()
+{
+ QFETCH(const QQmlDelegateModel::DelegateModelAccess, delegateModelAccess);
+
+ // the only way to get a list of gadgets into QML is via a QVariantList
+ const Gadget oldValue = Gadget{1, "one"};
+ QVariantList gadgets {
+ QVariant::fromValue(oldValue),
+ QVariant::fromValue(Gadget{2, "two"}),
+ };
+
+ auto view = makeView({
+ {"delegateModelAccess", delegateModelAccess},
+ {"model", QVariant::fromValue(gadgets)}
+ });
+ QVERIFY(view);
+
+ QObject *currentItem = nullptr;
+ QTRY_VERIFY(currentItem = view->rootObject()->property("currentItem").value<QObject *>());
+ auto currentData = currentItem->property("modelData");
+ QCOMPARE(currentData.value<Gadget>(), oldValue);
+ QCOMPARE(currentItem->property("text"), oldValue.text());
+
+ const Gadget newValue = Gadget{42, "fortytwo"};
+ QMetaObject::invokeMethod(currentItem, "setModelData", QVariant::fromValue(newValue));
+ currentData = currentItem->property("modelData");
+ QCOMPARE(currentData.value<Gadget>(), newValue);
+ // replacing the gadget on the QML side updates bindings to modelData
+ QCOMPARE(currentItem->property("number"), newValue.number());
+ // but not required properties
+ QCOMPARE(currentItem->property("text"), oldValue.text());
+
+ // but since nothing can be written back, changes will not outlive the delegate
+ QCOMPARE(gadgets.at(0).value<Gadget>(), oldValue);
+}
+
+// The first two tests are for reference, documenting how modelData works with
+// lists as models.
+void tst_QQmlRangeModel::rangeModelTest_data()
+{
+ QTest::addColumn<QQmlDelegateModel::DelegateModelAccess>("delegateModelAccess");
+ QTest::addColumn<bool>("writeBack");
+
+ QTest::addRow("ReadOnly")
+ << QQmlDelegateModel::ReadOnly << false;
+ QTest::addRow("ReadWrite")
+ << QQmlDelegateModel::ReadWrite << true;
+}
+
+void tst_QQmlRangeModel::intRange()
+{
+ QFETCH(const QQmlDelegateModel::DelegateModelAccess, delegateModelAccess);
+ QFETCH(const bool, writeBack);
+
+ const int oldValue = 42;
+ std::vector<int> data{oldValue};
+ RangeModel model(&data);
+
+ auto view = makeView({
+ {"delegateModelAccess", delegateModelAccess},
+ {"model", QVariant::fromValue(&model)}
+ });
+
+ QVERIFY(view);
+ QObject *currentItem = nullptr;
+ QTRY_VERIFY(currentItem = view->rootObject()->property("currentItem").value<QObject *>());
+ QCOMPARE(currentItem->property("currentValue"), oldValue);
+
+ // nothing happened so far, so there shouldn't have been any calls to setData
+ QEXPECT_FAIL("ReadWrite", "Unexpected call to setData", Continue);
+ QCOMPARE(model.setDataCalls, QList<int>{});
+ model.setDataCalls.clear();
+ model.dataCalls.clear();
+
+ // Changing the data via QAIM api...
+ const QModelIndex index = model.index(0, 0);
+ const QVariant newValue = 7;
+ QVERIFY(model.setData(index, newValue, Qt::RangeModelDataRole)); // default: Qt::EditRole
+ // ... should give us one call to setData (our own)
+ QEXPECT_FAIL("ReadWrite", "Unexpected call to setData", Continue); // but we get two
+ QCOMPARE(model.setDataCalls, QList<int>{Qt::RangeModelDataRole});
+ model.setDataCalls.clear();
+ // ... and results in a single call to data() to get the new value
+ QCOMPARE(model.dataCalls, QList<int>{Qt::RangeModelDataRole});
+ model.dataCalls.clear();
+ // ... which updates the QML side
+ QCOMPARE(currentItem->property("currentValue"), newValue);
+
+ // The delegate changing the property ...
+ QMetaObject::invokeMethod(currentItem, "setValue", oldValue);
+ // ... should result in a single call to QRM::data()
+ QEXPECT_FAIL("ReadWrite", "Extra call to data()", Continue); // but we see two
+ QCOMPARE(model.dataCalls, writeBack ? QList<int>{Qt::RangeModelDataRole} : QList<int>{});
+ // ... and one call to setData if access mode is ReadWrite
+ QCOMPARE(model.setDataCalls, writeBack ? QList<int>{Qt::RangeModelDataRole} : QList<int>{});
+ // ... which writes back to the model and updates our data structure
+ QCOMPARE(model.data(index) == oldValue, writeBack);
+ QCOMPARE(data.at(0) == oldValue, writeBack);
+}
+
+void tst_QQmlRangeModel::objectRange()
+{
+ QFETCH(const QQmlDelegateModel::DelegateModelAccess, delegateModelAccess);
+ QFETCH(const bool, writeBack);
+
+ QPointer<Entry> entry = new Entry(1, "one");
+ std::vector<Entry *> objects{entry.get()};
+ RangeModel model(&objects);
+
+ // with ReadWrite, spurious call to setData(RangeModelDataRole) during loading
+ if (writeBack) {
+ QTest::ignoreMessage(QtCriticalMsg,
+ QRegularExpression("Not able to assign QVariant\\(.*\\) to Entry*"));
+ }
+
+ auto view = makeView({
+ {"delegateModelAccess", delegateModelAccess},
+ {"model", QVariant::fromValue(&model)}
+ });
+
+ QVERIFY(view);
+ QObject *currentItem = nullptr;
+ QTRY_VERIFY(currentItem = view->rootObject()->property("currentItem").value<QObject *>());
+
+ // loading should call data() for all bound properties
+ QVERIFY(model.dataCalls.contains(Entry::NumberRole));
+ QVERIFY(model.dataCalls.contains(Qt::RangeModelDataRole));
+ model.dataCalls.clear();
+ // there shouldn't have been any attempts to write yet
+ QEXPECT_FAIL("ReadWrite", "Premature calls to setData()", Continue);
+ QCOMPARE(model.setDataCalls, QList<int>{});
+ model.setDataCalls.clear();
+
+ const QModelIndex index = model.index(0, 0);
+ const QVariant oldNumber = entry->number();
+ const QVariant newNumber = 2;
+ // Changing bound-to data via QAIM API...
+ model.setData(index, newNumber, Entry::NumberRole);
+ // .. calls data once, for that role
+ QCOMPARE(model.dataCalls, QList<int>{Entry::NumberRole});
+ // ... to update the QML properties
+ QCOMPARE(currentItem->property("number"), newNumber);
+ QCOMPARE(currentItem->property("modelNumber"), newNumber);
+ // ... and there should only be our call to setData
+ QEXPECT_FAIL("ReadWrite", "Extra call to setData()", Continue);
+ QCOMPARE(model.setDataCalls, QList<int>{Entry::NumberRole});
+ model.setDataCalls.clear();
+ model.dataCalls.clear();
+
+ // changing a property on the QML side ...
+ QMetaObject::invokeMethod(currentItem, "setValue", oldNumber);
+ // ... should call QRM::setData for the changed role, if write back is enabled
+ QCOMPARE(model.setDataCalls, writeBack ? QList<int>{Entry::NumberRole} : QList<int>{});
+ // ... to update our model, and the backing QObject
+ QCOMPARE(entry->number(), writeBack ? oldNumber : newNumber);
+ QCOMPARE(currentItem->property("number"), oldNumber);
+ // ... and call QRM::data, once, to get the new value
+ QEXPECT_FAIL("ReadWrite", "Excessive calls to data()", Continue);
+ QCOMPARE(model.dataCalls, writeBack ? QList<int>{Entry::NumberRole} : QList<int>{});
+ model.dataCalls.clear();
+ model.setDataCalls.clear();
+}
+
+void tst_QQmlRangeModel::gadgetRange()
+{
+ QFETCH(const QQmlDelegateModel::DelegateModelAccess, delegateModelAccess);
+ QFETCH(const bool, writeBack);
+
+ Gadget oldValue = {1, "one"};
+ std::vector<Gadget> gadgets{oldValue};
+ RangeModel model(&gadgets);
+
+ auto view = makeView({
+ {"delegateModelAccess", delegateModelAccess},
+ {"model", QVariant::fromValue(&model)}
+ });
+
+ QVERIFY(view);
+ QObject *currentItem = nullptr;
+ QTRY_VERIFY(currentItem = view->rootObject()->property("currentItem").value<QObject *>());
+ auto currentData = currentItem->property("modelData");
+ QCOMPARE(currentData.value<Gadget>(), oldValue);
+ QCOMPARE(currentItem->property("text"), oldValue.text());
+
+ // setting modelData on the QML side...
+ const Gadget newValue = Gadget{42, "fortytwo"};
+ QMetaObject::invokeMethod(currentItem, "setModelData", QVariant::fromValue(newValue));
+ currentData = currentItem->property("modelData");
+ QCOMPARE(currentData.value<Gadget>(), newValue);
+ // ... updates bindings to modelData
+ QCOMPARE(currentItem->property("number"), newValue.number());
+ // ... and, with ReadWrite, required properties
+ QCOMPARE(currentItem->property("text"), writeBack ? newValue.text() : oldValue.text());
+ // ... as well as the C++ data storage
+ QCOMPARE(gadgets.at(0), writeBack ? newValue : oldValue);
+
+ // updating the model using QAIM API updates all QML properties,
+ // in all access modes
+ const Gadget newestValue = Gadget(2, "two");
+ const QModelIndex index = model.index(0, 0);
+ QVERIFY(model.setData(index, QVariant::fromValue(newestValue), Qt::RangeModelDataRole));
+ QCOMPARE(currentItem->property("text"), newestValue.text());
+ QCOMPARE(currentItem->property("number"), newestValue.number());
+
+ // updating a required property on the QML side...
+ const QString newText = "three";
+ QMetaObject::invokeMethod(currentItem, "setValue", QVariant(newText));
+ // ... updates the model for ReadWrite access.
+ QCOMPARE(gadgets.at(0).text(), writeBack ? newText : newestValue.text());
+}
+
+void tst_QQmlRangeModel::gadgetTable()
+{
+ QFETCH(const QQmlDelegateModel::DelegateModelAccess, delegateModelAccess);
+ QFETCH(const bool, writeBack);
+
+ Gadget oldGadget = {11, "1.a"};
+ std::vector<std::pair<Gadget, Gadget>> gadgets{
+ {oldGadget, {12, "1.b"}},
+ {{21, "2.a"}, {22, "2.b"}},
+ };
+ RangeModel model(&gadgets);
+
+ auto view = makeView({
+ {"delegateModelAccess", delegateModelAccess},
+ {"model", QVariant::fromValue(&model)}
+ });
+
+ QVERIFY(view);
+ QObject *currentItem = nullptr;
+ QTRY_VERIFY(currentItem = view->rootObject()->property("currentItem").value<QObject *>());
+ auto currentData = currentItem->property("text");
+
+ auto *selectionModel = view->rootObject()->property("selectionModel").value<QItemSelectionModel *>();
+ QVERIFY(selectionModel);
+ const QModelIndex index = selectionModel->currentIndex();
+ QVERIFY(index.isValid());
+
+ const QString oldText = gadgets.at(0).second.text();
+ const QString newText = "1.A";
+
+ // updating data via QAIM API
+ model.setData(index, newText, Gadget::TextRole);
+ // ... updates delegate
+ QCOMPARE(currentItem->property("text"), newText);
+ // ... and C++ data
+ QCOMPARE(gadgets.at(0).first.text(), newText);
+
+ // updating properties in QML
+ QMetaObject::invokeMethod(currentItem, "setValue", oldText);
+ // ... updates model and C++ in ReadWrite access mode
+ QCOMPARE(model.data(index, Gadget::TextRole), writeBack ? oldText : newText);
+ QCOMPARE(gadgets.at(0).first.text(), writeBack ? oldText : newText);
+
+ // replaceing the gadget via QAIM API
+ Gadget newGadget{33, "3.c"};
+ model.setData(index, QVariant::fromValue(newGadget), Qt::RangeModelDataRole);
+ // ... updates delegate and C++
+ QCOMPARE(currentItem->property("modelData").value<Gadget>(), newGadget);
+ QCOMPARE(gadgets.at(0).first, newGadget);
+
+ // updating the gadget in QML
+ QMetaObject::invokeMethod(currentItem, "setModelData", QVariant::fromValue(oldGadget));
+ // ... updates the model and C++ in ReadWrite access mode
+ QCOMPARE(model.data(index, Qt::RangeModelDataRole).value<Gadget>(),
+ writeBack ? oldGadget : newGadget);
+
+ // updating a gadget property in QML
+ QMetaObject::invokeMethod(currentItem, "setModelDataNumber", 42);
+ // ... modifies the local copy and does nothing
+ QCOMPARE(model.data(index, Qt::RangeModelDataRole).value<Gadget>(),
+ writeBack ? oldGadget : newGadget);
+}
+
+QTEST_MAIN(tst_QQmlRangeModel)
+
+#include "tst_qqmlrangemodel.moc"