diff options
Diffstat (limited to 'tests/auto/qml/qqmldelegatemodel')
15 files changed, 1037 insertions, 4 deletions
diff --git a/tests/auto/qml/qqmldelegatemodel/CMakeLists.txt b/tests/auto/qml/qqmldelegatemodel/CMakeLists.txt index 705ee6f357..8d8a90e0a7 100644 --- a/tests/auto/qml/qqmldelegatemodel/CMakeLists.txt +++ b/tests/auto/qml/qqmldelegatemodel/CMakeLists.txt @@ -7,6 +7,12 @@ ## tst_qqmldelegatemodel Test: ##################################################################### +if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + cmake_minimum_required(VERSION 3.16) + project(tst_qqmldelegatemodel LANGUAGES CXX) + find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST) +endif() + # Collect test data file(GLOB_RECURSE test_data_glob RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} diff --git a/tests/auto/qml/qqmldelegatemodel/data/abstractItemModel.qml b/tests/auto/qml/qqmldelegatemodel/data/abstractItemModel.qml index a61e94ea6d..5ef502601c 100644 --- a/tests/auto/qml/qqmldelegatemodel/data/abstractItemModel.qml +++ b/tests/auto/qml/qqmldelegatemodel/data/abstractItemModel.qml @@ -1,5 +1,5 @@ // Copyright (C) 2019 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only import QtQml.Models 2.15 import QtQuick 2.15 diff --git a/tests/auto/qml/qqmldelegatemodel/data/clearCacheDuringInsertion.qml b/tests/auto/qml/qqmldelegatemodel/data/clearCacheDuringInsertion.qml new file mode 100644 index 0000000000..37e508302e --- /dev/null +++ b/tests/auto/qml/qqmldelegatemodel/data/clearCacheDuringInsertion.qml @@ -0,0 +1,138 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import QtQuick +import QtQuick.Window +import QtQuick.Controls +import QtQuick.Layouts +import QtQml.Models + +Window { + id: root + width: 640 + height: 480 + visible: true + color: "#111111" + + Column { + spacing: 1 + Repeater { + model: 1000 + + Rectangle { + width: 100 + height: 100 + color: "grey" + DelegateModel { + id: delegateModel + delegate: Rectangle { + width: 100 + height: 20 + color: "black" + Text { + anchors.centerIn: parent + text: "Name: " + model.name + color: "white" + } + } + + property int length: 0 + property var filterAcceptsItem: function(item) { return true; } + + model: ListModel { + id: myModel + + ListElement { + name: "tomato" + classifications: [ + ListElement { classification: "fruit" }, + ListElement { classification: "veg" } + ] + nutritionFacts: [ + ListElement { + calories: "45" + } + ] + } + + ListElement { + name: "apple" + classifications: [ + ListElement { classification: "fruit" } + ] + nutritionFacts: [ + ListElement { + calories: "87" + } + ] + } + + ListElement { + name: "broccoli" + classifications: [ + ListElement { classification: "veg" } + ] + nutritionFacts: [ + ListElement { + calories: "12" + } + ] + } + + ListElement { + name: "squash" + classifications: [ + ListElement { classification: "veg" } + ] + nutritionFacts: [ + ListElement { + calories: "112" + } + ] + } + } + + groups: [ + DelegateModelGroup { id: visibleItems; name: "visible" }, + DelegateModelGroup { name: "veg" }, + DelegateModelGroup { name: "fruit"; includeByDefault: true } + ] + + function update() { + + // Step 1: Filter items + var visible = []; + for (var i = 0; i < items.count; ++i) { + var item = items.get(i); + if (filterAcceptsItem(item.model)) { + visible.push(item); + } + } + + // Step 2: Add all items to the visible group: + for (i = 0; i < visible.length; ++i) { + items.insert(visible[i], delegateModel.filterOnGroup) + } + delegateModel.length = visible.length + } + + items.onChanged: update() + onFilterAcceptsItemChanged: update() + + filterOnGroup: "visible" + Component.onCompleted: { + for(var i = 0; i < myModel.count; i++) { + var temp = 0; + var entry = myModel.get(i); + + for (var j = 0; j < entry.classifications.count; j++) { + temp = entry.classifications.get(j) + items.insert(entry, temp.classification) + } + } + } + } + } + } + } +} diff --git a/tests/auto/qml/qqmldelegatemodel/data/deleteRace.qml b/tests/auto/qml/qqmldelegatemodel/data/deleteRace.qml new file mode 100644 index 0000000000..b01b14f7b8 --- /dev/null +++ b/tests/auto/qml/qqmldelegatemodel/data/deleteRace.qml @@ -0,0 +1,50 @@ +import QtQuick +import QtQml.Models + +Item { + DelegateModel { + id: delegateModel + model: ListModel { + id: sourceModel + + ListElement { title: "foo" } + ListElement { title: "bar" } + + function clear() { + if (count > 0) + remove(0, count); + } + } + + groups: [ + DelegateModelGroup { name: "selectedItems" } + ] + + delegate: Text { + height: DelegateModel.inSelectedItems ? implicitHeight * 2 : implicitHeight + Component.onCompleted: { + if (index === 0) + DelegateModel.inSelectedItems = true; + } + } + + Component.onCompleted: { + items.create(0) + items.create(1) + } + } + + ListView { + anchors.fill: parent + model: delegateModel + } + + Timer { + running: true + interval: 10 + onTriggered: sourceModel.clear() + } + + property int count: delegateModel.items.count +} + diff --git a/tests/auto/qml/qqmldelegatemodel/data/integerModel.qml b/tests/auto/qml/qqmldelegatemodel/data/integerModel.qml index 42bf882fda..bcc7a2786a 100644 --- a/tests/auto/qml/qqmldelegatemodel/data/integerModel.qml +++ b/tests/auto/qml/qqmldelegatemodel/data/integerModel.qml @@ -1,5 +1,5 @@ // Copyright (C) 2019 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only import QtQml.Models 2.15 import QtQuick 2.15 diff --git a/tests/auto/qml/qqmldelegatemodel/data/listModel.qml b/tests/auto/qml/qqmldelegatemodel/data/listModel.qml index f74abb2e9a..b4ea7a283f 100644 --- a/tests/auto/qml/qqmldelegatemodel/data/listModel.qml +++ b/tests/auto/qml/qqmldelegatemodel/data/listModel.qml @@ -1,5 +1,5 @@ // Copyright (C) 2019 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only import QtQml.Models 2.15 import QtQuick 2.15 diff --git a/tests/auto/qml/qqmldelegatemodel/data/modifyObjectUnderConstruction.qml b/tests/auto/qml/qqmldelegatemodel/data/modifyObjectUnderConstruction.qml new file mode 100644 index 0000000000..f21577e395 --- /dev/null +++ b/tests/auto/qml/qqmldelegatemodel/data/modifyObjectUnderConstruction.qml @@ -0,0 +1,18 @@ +import QtQuick + +Window { + id: window + width: 640 + height: 480 + visible: true + property alias testModel: repeater.model + + Repeater { + id: repeater + model: 1 + delegate: Item { + Component.onCompleted: repeater.model = 0 + } + } +} + diff --git a/tests/auto/qml/qqmldelegatemodel/data/overriddenModelData.qml b/tests/auto/qml/qqmldelegatemodel/data/overriddenModelData.qml new file mode 100644 index 0000000000..e392b2e5c9 --- /dev/null +++ b/tests/auto/qml/qqmldelegatemodel/data/overriddenModelData.qml @@ -0,0 +1,55 @@ +import QtQml + +DelegateModel { + id: root + + property ListModel listModel: ListModel { + ListElement { + modelData: "a" + row: "b" + column: "c" + model: "d" + hasModelChildren: "e" + index: "f" + } + } + + property var array: [{ + modelData: "a", + row: "b", + column: "c", + model: "d", + hasModelChildren: "e", + index: "f" + }] + + property QtObject object: QtObject { + property string modelData: "a" + property string row: "b" + property string column: "c" + property string model: "d" + property string hasModelChildren: "e" + property string index: "f" + } + + property int n: -1 + + model: { + switch (n) { + case 0: return listModel + case 1: return array + case 2: return object + } + return undefined; + } + + delegate: QtObject { + required property string modelData + required property string row + required property string column + required property string model + required property string hasModelChildren + required property string index + objectName: [modelData, row, column, model, hasModelChildren, index].join(" ") + } +} diff --git a/tests/auto/qml/qqmldelegatemodel/data/persistedItemsCache.qml b/tests/auto/qml/qqmldelegatemodel/data/persistedItemsCache.qml new file mode 100644 index 0000000000..5ae2038e1f --- /dev/null +++ b/tests/auto/qml/qqmldelegatemodel/data/persistedItemsCache.qml @@ -0,0 +1,62 @@ +import QtQuick +import QtQuick.Window +import QtQml.Models + +Window { + id: win + visible: true + width: 640 + height: 480 + property int destroyCount : 0; + property int createCount : 0; + property alias testListModel: mdl + + DelegateModel { + id: visualModel + model: ListModel { + id: mdl + ListElement { + name: "a" + hidden: false + } + ListElement { + name: "b" + hidden: true + } + ListElement { + name: "c" + hidden: false + } + } + + filterOnGroup: "selected" + + groups: [ + DelegateModelGroup { + name: "selected" + includeByDefault: true + } + ] + + delegate: Text { + visible: DelegateModel.inSelected + property var idx + Component.onCompleted: { + ++createCount + idx = index + DelegateModel.inPersistedItems = true + DelegateModel.inSelected = !model.hidden + } + Component.onDestruction: ++destroyCount + text: model.name + } + } + + ListView { + id: listView + model: visualModel + anchors.fill: parent + focus: true + } + +} diff --git a/tests/auto/qml/qqmldelegatemodel/data/requiredModelData.qml b/tests/auto/qml/qqmldelegatemodel/data/requiredModelData.qml new file mode 100644 index 0000000000..467d60dff8 --- /dev/null +++ b/tests/auto/qml/qqmldelegatemodel/data/requiredModelData.qml @@ -0,0 +1,50 @@ +import QtQml + +DelegateModel { + id: root + + property ListModel singularModel: ListModel { + ListElement { + a: "a" + } + ListElement { + a: "a" + } + } + + property ListModel listModel: ListModel { + ListElement { + a: "a" + b: "b" + } + ListElement { + a: "a" + b: "b" + } + } + + property var array: [ + {a: "a", b: "b"}, {a: "b", b: "b"} + ] + + property QtObject object: QtObject { + property string a: "a" + property string b: "b" + } + + property int n: -1 + + model: { + switch (n) { + case 0: return singularModel + case 1: return listModel + case 2: return array + case 3: return object + } + return undefined; + } + + delegate: QtObject { + required property string a + } +} diff --git a/tests/auto/qml/qqmldelegatemodel/data/resetModelData.qml b/tests/auto/qml/qqmldelegatemodel/data/resetModelData.qml new file mode 100644 index 0000000000..1fe6168c79 --- /dev/null +++ b/tests/auto/qml/qqmldelegatemodel/data/resetModelData.qml @@ -0,0 +1,16 @@ +import QtQuick + +ListView { + id: root + anchors.fill: parent + property bool success: (currentItem?.mydata ?? 0) === 42 + height: 300 + width: 200 + + delegate: Rectangle { + required property var model + implicitWidth: 100 + implicitHeight: 50 + property var mydata: model?.foo ?? model.bar + } +} diff --git a/tests/auto/qml/qqmldelegatemodel/data/typedModelData.qml b/tests/auto/qml/qqmldelegatemodel/data/typedModelData.qml new file mode 100644 index 0000000000..08f1c7d68e --- /dev/null +++ b/tests/auto/qml/qqmldelegatemodel/data/typedModelData.qml @@ -0,0 +1,64 @@ +import QtQml + +DelegateModel { + id: root + + // useful object as model, int as modelData + property ListModel singularModel: ListModel { + ListElement { + x: 11 + } + ListElement { + x: 12 + } + } + + // same, useful, object as model and modelData + property ListModel listModel: ListModel { + ListElement { + x: 13 + y: 14 + } + ListElement { + x: 15 + y: 16 + } + } + + // useful but different objects as modelData and model + // This is how the array accessor works. We can live with it. + property var array: [ + {x: 17, y: 18}, {x: 19, y: 20} + ] + + // useful but different objects as modelData and model + // This is how the object accessor works. We can live with it. + property QtObject object: QtObject { + property int x: 21 + property int y: 22 + } + + property int n: -1 + + model: { + switch (n) { + case 0: return singularModel + case 1: return listModel + case 2: return array + case 3: return object + } + return undefined; + } + + delegate: QtObject { + required property point modelData + required property QtObject model + + property real modelX: model.x + property real modelDataX: modelData.x + property point modelSelf: model + property point modelDataSelf: modelData + property point modelModelData: model.modelData + property point modelAnonymous: model[""] + } +} diff --git a/tests/auto/qml/qqmldelegatemodel/data/universalModelData.qml b/tests/auto/qml/qqmldelegatemodel/data/universalModelData.qml new file mode 100644 index 0000000000..f24009f873 --- /dev/null +++ b/tests/auto/qml/qqmldelegatemodel/data/universalModelData.qml @@ -0,0 +1,72 @@ +import QtQml + +DelegateModel { + id: root + + // useful object as model, string as modelData + property ListModel singularModel: ListModel { + ListElement { + a: "a" + } + ListElement { + a: "b" + } + } + + // same, useful, object as model and modelData + property ListModel listModel: ListModel { + ListElement { + a: "a" + b: "a" + } + ListElement { + a: "b" + b: "b" + } + } + + // useful but different objects as modelData and model + // This is how the array accessor works. We can live with it. + property var array: [ + {a: "a", b: "a"}, {a: "b", b: "a"} + ] + + // string as modelData + // object with anonymous string property as model. + property var stringList: ["a", "b"] + + // useful but different objects as modelData and model + // This is how the object accessor works. We can live with it. + property QtObject object: QtObject { + property string a: "a" + property string b: "a" + } + + // number as modelData + // object with anonymous number property as model + property int n: -1 + + model: { + switch (n) { + case 0: return singularModel + case 1: return listModel + case 2: return array + case 3: return stringList + case 4: return object + case 5: return n + } + return undefined; + } + + delegate: QtObject { + required property var modelData + required property var model + + property var modelA: model.a + property var modelDataA: modelData.a + property var modelSelf: model + property var modelDataSelf: modelData + property var modelModelData: model.modelData + property var modelAnonymous: model[""] + } +} diff --git a/tests/auto/qml/qqmldelegatemodel/data/viewUpdatedOnDelegateChoiceAffectingRoleChange.qml b/tests/auto/qml/qqmldelegatemodel/data/viewUpdatedOnDelegateChoiceAffectingRoleChange.qml new file mode 100644 index 0000000000..b2367b3a6e --- /dev/null +++ b/tests/auto/qml/qqmldelegatemodel/data/viewUpdatedOnDelegateChoiceAffectingRoleChange.qml @@ -0,0 +1,93 @@ +import QtQuick +import Qt.labs.qmlmodels +import QtQml.Models + +Item { + id: root + property bool triggered: false + onTriggeredChanged: { + rootLM.setProperty(1, "currentRole", "first"); + } + width: 800 + height: 680 + + function verify(): bool { + rootLV.currentIndex = 1; // needed for itemAtIndex to work + if (root.triggered) + return rootLV.itemAtIndex(0).isFirst && rootLV.itemAtIndex(1).isFirst; + else + return rootLV.itemAtIndex(0).isFirst && !rootLV.itemAtIndex(1).isFirst; + } + + ListModel { + id: rootLM + ListElement { + currentRole: "first" + firstText: "TEXT_FIRST_1" + secondText: "TEXT_SECOND_1" + } + ListElement { + currentRole: "second" + firstText: "TEXT_FIRST_2" + secondText: "TEXT_SECOND_2" + } + } + + DelegateModel { + id: delModel + model: rootLM + delegate: DelegateChooser { + id: delegateChooser + role: "currentRole" + DelegateChoice { + roleValue: "first" + Rectangle { + property bool isFirst: true + height: 30 + width: rootLV.width + color: "yellow" + Text { + anchors.centerIn: parent + text: firstText + " " + currentRole + } + } + } + DelegateChoice { + roleValue: "second" + Rectangle { + property bool isFirst: false + height: 30 + width: rootLV.width + color: "red" + Text { + anchors.centerIn: parent + text: secondText + " " + currentRole + } + } + } + } + } + + TapHandler { + // for manual testing + onTapped: root.triggered = true + } + + Rectangle { + width: 200 + height: 300 + anchors.centerIn: parent + border.color: "black" + border.width: 1 + color: "blue" + + ListView { + id: rootLV + objectName: "listview" + anchors.margins: 30 + anchors.fill: parent + cacheBuffer: 0 + model: delModel + } + } +} diff --git a/tests/auto/qml/qqmldelegatemodel/tst_qqmldelegatemodel.cpp b/tests/auto/qml/qqmldelegatemodel/tst_qqmldelegatemodel.cpp index ca66ddb618..2cacda5513 100644 --- a/tests/auto/qml/qqmldelegatemodel/tst_qqmldelegatemodel.cpp +++ b/tests/auto/qml/qqmldelegatemodel/tst_qqmldelegatemodel.cpp @@ -1,16 +1,21 @@ // Copyright (C) 2019 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include <QtTest/qtest.h> +#include <QtCore/qjsonobject.h> #include <QtCore/QConcatenateTablesProxyModel> #include <QtGui/QStandardItemModel> #include <QtQml/qqmlcomponent.h> +#include <QtQml/qqmlapplicationengine.h> #include <QtQmlModels/private/qqmldelegatemodel_p.h> +#include <QtQmlModels/private/qqmllistmodel_p.h> #include <QtQuick/qquickview.h> #include <QtQuick/qquickitem.h> #include <QtQuickTestUtils/private/qmlutils_p.h> #include <QtTest/QSignalSpy> +#include <forward_list> + class tst_QQmlDelegateModel : public QQmlDataTest { Q_OBJECT @@ -19,6 +24,7 @@ public: tst_QQmlDelegateModel(); private slots: + void resettingRolesRespected(); void valueWithoutCallingObjectFirst_data(); void valueWithoutCallingObjectFirst(); void qtbug_86017(); @@ -26,6 +32,16 @@ private slots: void contextAccessedByHandler(); void redrawUponColumnChange(); void nestedDelegates(); + void universalModelData(); + void typedModelData(); + void requiredModelData(); + void overriddenModelData(); + void deleteRace(); + void persistedItemsStayInCache(); + void unknownContainersAsModel(); + void doNotUnrefObjectUnderConstruction(); + void clearCacheDuringInsertion(); + void viewUpdatedOnDelegateChoiceAffectingRoleChange(); }; class AbstractItemModel : public QAbstractItemModel @@ -85,6 +101,61 @@ tst_QQmlDelegateModel::tst_QQmlDelegateModel() qmlRegisterType<AbstractItemModel>("Test", 1, 0, "AbstractItemModel"); } +class TableModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + int rowCount(const QModelIndex & = QModelIndex()) const override + { + return 1; + } + + int columnCount(const QModelIndex & = QModelIndex()) const override + { + return 1; + } + + QVariant data(const QModelIndex &index, int role) const override + { + switch (role) { + case 0: + return QString("foo: %1, %2").arg(index.column()).arg(index.row()); + case 1: + return 42; + default: + break; + } + + return QVariant(); + } + + Q_INVOKABLE void change() { beginResetModel(); toggle = !toggle; endResetModel(); } + + QHash<int, QByteArray> roleNames() const override + { + if (toggle) + return { {0, "foo"} }; + else + return { {1, "bar"} }; + } + + bool toggle = true; +}; + +void tst_QQmlDelegateModel::resettingRolesRespected() +{ + auto model = std::make_unique<TableModel>(); + QQmlApplicationEngine engine; + engine.setInitialProperties({ {"model", QVariant::fromValue(model.get()) }} ); + engine.load(testFileUrl("resetModelData.qml")); + QTRY_VERIFY(!engine.rootObjects().isEmpty()); + QObject *root = engine.rootObjects().constFirst(); + QVERIFY(!root->property("success").toBool()); + model->change(); + QTRY_VERIFY(root->property("success").toBool()); +} + void tst_QQmlDelegateModel::valueWithoutCallingObjectFirst_data() { QTest::addColumn<QUrl>("qmlFileUrl"); @@ -207,6 +278,344 @@ void tst_QQmlDelegateModel::nestedDelegates() QFAIL("Loader not found"); } +void tst_QQmlDelegateModel::universalModelData() +{ + QQmlEngine engine; + QQmlComponent c(&engine, testFileUrl("universalModelData.qml")); + QVERIFY2(c.isReady(), qPrintable(c.errorString())); + QScopedPointer<QObject> o(c.create()); + + QQmlDelegateModel *delegateModel = qobject_cast<QQmlDelegateModel *>(o.data()); + QVERIFY(delegateModel); + + for (int i = 0; i < 6; ++i) { + delegateModel->setProperty("n", i); + QObject *delegate = delegateModel->object(0); + QObject *modelItem = delegate->property("modelSelf").value<QObject *>(); + QVERIFY(modelItem != nullptr); + switch (i) { + case 0: { + // list model with 1 role + QCOMPARE(delegate->property("modelA"), QStringLiteral("a")); + QVERIFY(!delegate->property("modelDataA").isValid()); + QCOMPARE(delegate->property("modelDataSelf"), QStringLiteral("a")); + QCOMPARE(delegate->property("modelModelData"), QStringLiteral("a")); + QCOMPARE(delegate->property("modelAnonymous"), QStringLiteral("a")); + break; + } + case 1: { + // list model with 2 roles + QCOMPARE(delegate->property("modelA"), QStringLiteral("a")); + QCOMPARE(delegate->property("modelDataA"), QStringLiteral("a")); + QCOMPARE(delegate->property("modelDataSelf"), QVariant::fromValue(modelItem)); + QCOMPARE(delegate->property("modelModelData"), QVariant::fromValue(modelItem)); + QCOMPARE(delegate->property("modelAnonymous"), QVariant::fromValue(modelItem)); + break; + } + case 2: { + // JS array of objects + QCOMPARE(delegate->property("modelA"), QStringLiteral("a")); + QCOMPARE(delegate->property("modelDataA"), QStringLiteral("a")); + + // Do the comparison in QVariantMap. The values get converted back and forth a + // few times, making any JavaScript equality comparison impossible. + // This is only due to test setup, though. + const QVariantMap modelData = delegate->property("modelDataSelf").value<QVariantMap>(); + QVERIFY(!modelData.isEmpty()); + QCOMPARE(delegate->property("modelModelData").value<QVariantMap>(), modelData); + QCOMPARE(delegate->property("modelAnonymous").value<QVariantMap>(), modelData); + break; + } + case 3: { + // string list + QVERIFY(!delegate->property("modelA").isValid()); + QVERIFY(!delegate->property("modelDataA").isValid()); + QCOMPARE(delegate->property("modelDataSelf"), QStringLiteral("a")); + QCOMPARE(delegate->property("modelModelData"), QStringLiteral("a")); + QCOMPARE(delegate->property("modelAnonymous"), QStringLiteral("a")); + break; + } + case 4: { + // single object + QCOMPARE(delegate->property("modelA"), QStringLiteral("a")); + QCOMPARE(delegate->property("modelDataA"), QStringLiteral("a")); + QObject *modelData = delegate->property("modelDataSelf").value<QObject *>(); + QVERIFY(modelData != nullptr); + QCOMPARE(delegate->property("modelModelData"), QVariant::fromValue(modelData)); + QCOMPARE(delegate->property("modelAnonymous"), QVariant::fromValue(modelData)); + break; + } + case 5: { + // a number + QVERIFY(!delegate->property("modelA").isValid()); + QVERIFY(!delegate->property("modelDataA").isValid()); + const QVariant modelData = delegate->property("modelDataSelf"); + + // This is int on 32bit systems because qsizetype fits into int there. + // On 64bit systems it's double because qsizetype doesn't fit into int. + if (sizeof(qsizetype) > sizeof(int)) + QCOMPARE(modelData.metaType(), QMetaType::fromType<double>()); + else + QCOMPARE(modelData.metaType(), QMetaType::fromType<int>()); + + QCOMPARE(modelData.value<int>(), 0); + QCOMPARE(delegate->property("modelModelData"), modelData); + QCOMPARE(delegate->property("modelAnonymous"), modelData); + break; + } + default: + QFAIL("wrong model number"); + break; + } + } + +} + +void tst_QQmlDelegateModel::typedModelData() +{ + QQmlEngine engine; + const QUrl url = testFileUrl("typedModelData.qml"); + QQmlComponent c(&engine, url); + QVERIFY2(c.isReady(), qPrintable(c.errorString())); + QScopedPointer<QObject> o(c.create()); + + QQmlDelegateModel *delegateModel = qobject_cast<QQmlDelegateModel *>(o.data()); + QVERIFY(delegateModel); + + for (int i = 0; i < 4; ++i) { + if (i == 0) { + for (int j = 0; j < 3; ++j) { + QTest::ignoreMessage( + QtWarningMsg, + "Could not find any constructor for value type QQmlPointFValueType " + "to call with value QVariant(double, 11)"); + } + + QTest::ignoreMessage( + QtWarningMsg, + qPrintable(url.toString() + ":62:9: Unable to assign double to QPointF")); + QTest::ignoreMessage( + QtWarningMsg, + qPrintable(url.toString() + ":61:9: Unable to assign double to QPointF")); + } + + delegateModel->setProperty("n", i); + QObject *delegate = delegateModel->object(0); + QVERIFY(delegate); + const QPointF modelItem = delegate->property("modelSelf").value<QPointF>(); + switch (i) { + case 0: { + // list model with 1 role. + // Does not work, for the most part, because the model is singular + QCOMPARE(delegate->property("modelX"), 11.0); + QCOMPARE(delegate->property("modelDataX"), 0.0); + QCOMPARE(delegate->property("modelSelf"), QPointF(11.0, 0.0)); + QCOMPARE(delegate->property("modelDataSelf"), QPointF()); + QCOMPARE(delegate->property("modelModelData"), QPointF()); + QCOMPARE(delegate->property("modelAnonymous"), QPointF()); + break; + } + case 1: { + // list model with 2 roles + QCOMPARE(delegate->property("modelX"), 13.0); + QCOMPARE(delegate->property("modelDataX"), 13.0); + QCOMPARE(delegate->property("modelSelf"), QVariant::fromValue(modelItem)); + QCOMPARE(delegate->property("modelDataSelf"), QVariant::fromValue(modelItem)); + QCOMPARE(delegate->property("modelModelData"), QVariant::fromValue(modelItem)); + QCOMPARE(delegate->property("modelAnonymous"), QVariant::fromValue(modelItem)); + break; + } + case 2: { + // JS array of objects + QCOMPARE(delegate->property("modelX"), 17.0); + QCOMPARE(delegate->property("modelDataX"), 17.0); + + const QPointF modelData = delegate->property("modelDataSelf").value<QPointF>(); + QCOMPARE(modelData, QPointF(17, 18)); + QCOMPARE(delegate->property("modelSelf"), QVariant::fromValue(modelData)); + QCOMPARE(delegate->property("modelModelData").value<QPointF>(), modelData); + QCOMPARE(delegate->property("modelAnonymous").value<QPointF>(), modelData); + break; + } + case 3: { + // single object + QCOMPARE(delegate->property("modelX"), 21); + QCOMPARE(delegate->property("modelDataX"), 21); + const QPointF modelData = delegate->property("modelDataSelf").value<QPointF>(); + QCOMPARE(modelData, QPointF(21, 22)); + QCOMPARE(delegate->property("modelSelf"), QVariant::fromValue(modelData)); + QCOMPARE(delegate->property("modelModelData"), QVariant::fromValue(modelData)); + QCOMPARE(delegate->property("modelAnonymous"), QVariant::fromValue(modelData)); + break; + } + default: + QFAIL("wrong model number"); + break; + } + } + +} + +void tst_QQmlDelegateModel::requiredModelData() +{ + QQmlEngine engine; + QQmlComponent c(&engine, testFileUrl("requiredModelData.qml")); + QVERIFY2(c.isReady(), qPrintable(c.errorString())); + QScopedPointer<QObject> o(c.create()); + + QQmlDelegateModel *delegateModel = qobject_cast<QQmlDelegateModel *>(o.data()); + QVERIFY(delegateModel); + + for (int i = 0; i < 4; ++i) { + delegateModel->setProperty("n", i); + QObject *delegate = delegateModel->object(0); + QVERIFY(delegate); + const QVariant a = delegate->property("a"); + QCOMPARE(a.metaType(), QMetaType::fromType<QString>()); + QCOMPARE(a.toString(), QLatin1String("a")); + } +} + +void tst_QQmlDelegateModel::overriddenModelData() +{ + QTest::failOnWarning(QRegularExpression( + "Final member [^ ]+ is overridden in class [^\\.]+. The override won't be used.")); + + QQmlEngine engine; + QQmlComponent c(&engine, testFileUrl("overriddenModelData.qml")); + QVERIFY2(c.isReady(), qPrintable(c.errorString())); + QScopedPointer<QObject> o(c.create()); + + QQmlDelegateModel *delegateModel = qobject_cast<QQmlDelegateModel *>(o.data()); + QVERIFY(delegateModel); + + for (int i = 0; i < 3; ++i) { + delegateModel->setProperty("n", i); + QObject *delegate = delegateModel->object(0); + QVERIFY(delegate); + + if (i == 1 || i == 2) { + // You can actually not override if the model is a QObject or a JavaScript array. + // Someone is certainly relying on this. + // We need to find a migration mechanism to fix it. + QCOMPARE(delegate->objectName(), QLatin1String(" 0 0 e 0")); + } else { + QCOMPARE(delegate->objectName(), QLatin1String("a b c d e f")); + } + } +} + +void tst_QQmlDelegateModel::deleteRace() +{ + QQmlEngine engine; + QQmlComponent c(&engine, testFileUrl("deleteRace.qml")); + QVERIFY2(c.isReady(), qPrintable(c.errorString())); + QScopedPointer<QObject> o(c.create()); + QVERIFY(!o.isNull()); + QTRY_COMPARE(o->property("count").toInt(), 2); + QTRY_COMPARE(o->property("count").toInt(), 0); +} + +void tst_QQmlDelegateModel::persistedItemsStayInCache() +{ + QQmlEngine engine; + QQmlComponent component(&engine, testFileUrl("persistedItemsCache.qml")); + QVERIFY2(component.isReady(), qPrintable(component.errorString())); + std::unique_ptr<QObject> object(component.create()); + QVERIFY(object); + const QVariant properyListModel = object->property("testListModel"); + QQmlListModel *listModel = qvariant_cast<QQmlListModel *>(properyListModel); + QVERIFY(listModel); + QTRY_COMPARE(object->property("createCount").toInt(), 3); + listModel->clear(); + QTRY_COMPARE(object->property("destroyCount").toInt(), 3); +} + +Q_DECLARE_SEQUENTIAL_CONTAINER_METATYPE(std::forward_list) +void tst_QQmlDelegateModel::unknownContainersAsModel() +{ + QQmlEngine engine; + + QQmlComponent modelComponent(&engine); + modelComponent.setData("import QtQml.Models\nDelegateModel {}\n", QUrl()); + QCOMPARE(modelComponent.status(), QQmlComponent::Ready); + + QScopedPointer<QObject> o(modelComponent.create()); + QQmlDelegateModel *delegateModel = qobject_cast<QQmlDelegateModel*>(o.data()); + QVERIFY(delegateModel); + + QQmlComponent delegateComponent(&engine); + delegateComponent.setData("import QtQml\nQtObject { objectName: modelData }\n", QUrl()); + QCOMPARE(delegateComponent.status(), QQmlComponent::Ready); + + delegateModel->setDelegate(&delegateComponent); + + QList<QJsonObject> json; + for (int i = 0; i < 10; ++i) + json.append(QJsonObject({{"foo", i}})); + + // Recognized as list + delegateModel->setModel(QVariant::fromValue(json)); + QObject *obj = delegateModel->object(0, QQmlIncubator::Synchronous); + QVERIFY(obj); + QCOMPARE(delegateModel->count(), 10); + QCOMPARE(delegateModel->model().metaType(), QMetaType::fromType<QList<QJsonObject>>()); + + QVERIFY(qMetaTypeId<std::forward_list<int>>() > 0); + std::forward_list<int> mess; + mess.push_front(4); + mess.push_front(5); + mess.push_front(6); + + // Converted into QVariantList + delegateModel->setModel(QVariant::fromValue(mess)); + obj = delegateModel->object(0, QQmlIncubator::Synchronous); + QVERIFY(obj); + QCOMPARE(delegateModel->count(), 3); + QCOMPARE(delegateModel->model().metaType(), QMetaType::fromType<QVariantList>()); +} + +void tst_QQmlDelegateModel::doNotUnrefObjectUnderConstruction() +{ + QQmlEngine engine; + QQmlComponent component(&engine, testFileUrl("modifyObjectUnderConstruction.qml")); + QVERIFY2(component.isReady(), qPrintable(component.errorString())); + std::unique_ptr<QObject> object(component.create()); + QVERIFY(object); + QTRY_COMPARE(object->property("testModel").toInt(), 0); +} + +void tst_QQmlDelegateModel::clearCacheDuringInsertion() +{ + QQmlEngine engine; + QQmlComponent component(&engine, testFileUrl("clearCacheDuringInsertion.qml")); + QVERIFY2(component.isReady(), qPrintable(component.errorString())); + std::unique_ptr<QObject> object(component.create()); + QVERIFY(object); + QTRY_COMPARE(object->property("testModel").toInt(), 0); +} + +void tst_QQmlDelegateModel::viewUpdatedOnDelegateChoiceAffectingRoleChange() +{ + QQmlEngine engine; + QQmlComponent component(&engine, testFileUrl("viewUpdatedOnDelegateChoiceAffectingRoleChange.qml")); + QVERIFY2(component.isReady(), qPrintable(component.errorString())); + std::unique_ptr<QObject> object(component.create()); + QVERIFY(object); + QQuickItem *listview = object->findChild<QQuickItem *>("listview"); + QVERIFY(listview); + QTRY_VERIFY(listview->property("count").toInt() > 0); + bool returnedValue = false; + QMetaObject::invokeMethod(object.get(), "verify", Q_RETURN_ARG(bool, returnedValue)); + QVERIFY(returnedValue); + returnedValue = false; + + object->setProperty("triggered", "true"); + QTRY_VERIFY(listview->property("count").toInt() > 0); + QMetaObject::invokeMethod(object.get(), "verify", Q_RETURN_ARG(bool, returnedValue)); + QVERIFY(returnedValue); +} + QTEST_MAIN(tst_QQmlDelegateModel) #include "tst_qqmldelegatemodel.moc" |