Add QML test for QRangeModel

We want QRangeModel to be the preferred mechanism for exposing C++ data
to QML. Ideally, it will also be a replacement for using QVariantList or
QObjectList, without the "quirks" of exposing a value type holding
references, modelData vs named roles, no write-back etc.

To make sure that the respective usage patterns don't break, add a test
for QVariantList and QObjectList, and corresponding tests that use
QRangeModel with equivalent data structures.

The test shows some unexpected and excessive calls to both data() and
setData() implementations of QRangeModel, which are QEXPECT_FAIL'ed for
now. Unrelated to this test of QRangeModel, so leaving that for a future
commit.

Pick-to: 6.10
Change-Id: I870c78d845de507dafd27f6999154a1a7ea92a1b
Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
This commit is contained in:
Volker Hilsheimer 2025-07-25 14:17:23 +02:00
parent 641050d442
commit aeaa7ae43e
10 changed files with 849 additions and 0 deletions

View File

@ -149,6 +149,7 @@ if(QT_FEATURE_private_tests)
add_subdirectory(qqmltranslation) add_subdirectory(qqmltranslation)
add_subdirectory(qqmlimport) add_subdirectory(qqmlimport)
add_subdirectory(qqmlobjectmodel) add_subdirectory(qqmlobjectmodel)
add_subdirectory(qqmlrangemodel)
add_subdirectory(qqmltablemodel) add_subdirectory(qqmltablemodel)
add_subdirectory(qqmlsortfilterproxymodel) add_subdirectory(qqmlsortfilterproxymodel)
add_subdirectory(qqmltreemodel) add_subdirectory(qqmltreemodel)

View File

@ -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"
)

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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
}
}
}

View File

@ -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;
}
}
}

View File

@ -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
}
}
}

View File

@ -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;
}
}
}

View File

@ -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"