// Copyright (C) 2019 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include #include #include #include #include #include #include #include #include #include #include #include #include class tst_QQmlDelegateModel : public QQmlDataTest { Q_OBJECT public: tst_QQmlDelegateModel(); private slots: void resettingRolesRespected(); void valueWithoutCallingObjectFirst_data(); void valueWithoutCallingObjectFirst(); void qtbug_86017(); void filterOnGroup_removeWhenCompleted(); 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 { Q_OBJECT public: AbstractItemModel() { for (int i = 0; i < 3; ++i) mValues.append(QString::fromLatin1("Item %1").arg(i)); } QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override { if (parent.isValid()) return QModelIndex(); return createIndex(row, column); } QModelIndex parent(const QModelIndex &) const override { return QModelIndex(); } int rowCount(const QModelIndex &parent = QModelIndex()) const override { if (parent.isValid()) return 0; return mValues.size(); } int columnCount(const QModelIndex &parent) const override { if (parent.isValid()) return 0; return 1; } QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override { if (role != Qt::DisplayRole) return QVariant(); return mValues.at(index.row()); } private: QVector mValues; }; tst_QQmlDelegateModel::tst_QQmlDelegateModel() : QQmlDataTest(QT_QMLTEST_DATADIR) { qmlRegisterType("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 roleNames() const override { if (toggle) return { {0, "foo"} }; else return { {1, "bar"} }; } bool toggle = true; }; void tst_QQmlDelegateModel::resettingRolesRespected() { auto model = std::make_unique(); 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("qmlFileUrl"); QTest::addColumn("index"); QTest::addColumn("role"); QTest::addColumn("expectedValue"); QTest::addRow("integer") << testFileUrl("integerModel.qml") << 50 << QString::fromLatin1("modelData") << QVariant(50); QTest::addRow("ListModel") << testFileUrl("listModel.qml") << 1 << QString::fromLatin1("name") << QVariant(QLatin1String("Item 1")); QTest::addRow("QAbstractItemModel") << testFileUrl("abstractItemModel.qml") << 1 << QString::fromLatin1("display") << QVariant(QLatin1String("Item 1")); } // Tests that it's possible to call variantValue() without creating // costly delegate items first via object(). void tst_QQmlDelegateModel::valueWithoutCallingObjectFirst() { QFETCH(const QUrl, qmlFileUrl); QFETCH(const int, index); QFETCH(const QString, role); QFETCH(const QVariant, expectedValue); QQmlEngine engine; QQmlComponent component(&engine); component.loadUrl(qmlFileUrl); QScopedPointer root(component.create()); QVERIFY2(root, qPrintable(component.errorString())); QQmlDelegateModel *model = qobject_cast(root.data()); QVERIFY(model); QCOMPARE(model->variantValue(index, role), expectedValue); } void tst_QQmlDelegateModel::qtbug_86017() { QQmlEngine engine; QQmlComponent component(&engine); component.loadUrl(testFileUrl("qtbug_86017.qml")); QScopedPointer root(component.create()); QVERIFY2(root, qPrintable(component.errorString())); QTRY_VERIFY(component.isReady()); QQmlDelegateModel *model = qobject_cast(root.data()); QVERIFY(model); QCOMPARE(model->count(), 2); QCOMPARE(model->filterGroup(), "selected"); } void tst_QQmlDelegateModel::filterOnGroup_removeWhenCompleted() { QQuickView view(testFileUrl("removeFromGroup.qml")); QCOMPARE(view.status(), QQuickView::Ready); view.show(); QQuickItem *root = view.rootObject(); QVERIFY(root); QQmlDelegateModel *model = root->findChild(); QVERIFY(model); QVERIFY(QTest::qWaitFor([=]{ return model->count() == 2; })); } void tst_QQmlDelegateModel::contextAccessedByHandler() { QQmlEngine engine; QQmlComponent component(&engine, testFileUrl("contextAccessedByHandler.qml")); QScopedPointer root(component.create()); QVERIFY2(root, qPrintable(component.errorString())); QVERIFY(root->property("works").toBool()); } void tst_QQmlDelegateModel::redrawUponColumnChange() { QStandardItemModel m1; m1.appendRow({ new QStandardItem("Banana"), new QStandardItem("Coconut"), }); QQuickView view(testFileUrl("redrawUponColumnChange.qml")); QCOMPARE(view.status(), QQuickView::Ready); view.show(); QQuickItem *root = view.rootObject(); root->setProperty("model", QVariant::fromValue(&m1)); QObject *item = root->property("currentItem").value(); QVERIFY(item); QCOMPARE(item->property("text").toString(), "Banana"); QVERIFY(root); m1.removeColumn(0); QCOMPARE(item->property("text").toString(), "Coconut"); } void tst_QQmlDelegateModel::nestedDelegates() { QQmlEngine engine; QQmlComponent c(&engine, testFileUrl("nestedDelegates.qml")); QVERIFY2(c.isReady(), qPrintable(c.errorString())); QScopedPointer o(c.create()); QQuickItem *item = qobject_cast(o.data()); QCOMPARE(item->childItems().size(), 2); for (QQuickItem *child : item->childItems()) { if (child->objectName() != QLatin1String("loader")) continue; QCOMPARE(child->childItems().size(), 1); QQuickItem *timeMarks = child->childItems().at(0); const QList children = timeMarks->childItems(); QCOMPARE(children.size(), 2); // One of them is the repeater, the other one is the rectangle QVERIFY(children.at(0)->objectName() == QLatin1String("zap") || children.at(1)->objectName() == QLatin1String("zap")); QVERIFY(children.at(0)->objectName().isEmpty() || children.at(1)->objectName().isEmpty()); return; // loader found } QFAIL("Loader not found"); } void tst_QQmlDelegateModel::universalModelData() { QQmlEngine engine; QQmlComponent c(&engine, testFileUrl("universalModelData.qml")); QVERIFY2(c.isReady(), qPrintable(c.errorString())); QScopedPointer o(c.create()); QQmlDelegateModel *delegateModel = qobject_cast(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(); 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(); QVERIFY(!modelData.isEmpty()); QCOMPARE(delegate->property("modelModelData").value(), modelData); QCOMPARE(delegate->property("modelAnonymous").value(), 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(); 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()); else QCOMPARE(modelData.metaType(), QMetaType::fromType()); QCOMPARE(modelData.value(), 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 o(c.create()); QQmlDelegateModel *delegateModel = qobject_cast(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(); 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(); QCOMPARE(modelData, QPointF(17, 18)); QCOMPARE(delegate->property("modelSelf"), QVariant::fromValue(modelData)); QCOMPARE(delegate->property("modelModelData").value(), modelData); QCOMPARE(delegate->property("modelAnonymous").value(), modelData); break; } case 3: { // single object QCOMPARE(delegate->property("modelX"), 21); QCOMPARE(delegate->property("modelDataX"), 21); const QPointF modelData = delegate->property("modelDataSelf").value(); 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 o(c.create()); QQmlDelegateModel *delegateModel = qobject_cast(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()); 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 o(c.create()); QQmlDelegateModel *delegateModel = qobject_cast(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 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 object(component.create()); QVERIFY(object); const QVariant properyListModel = object->property("testListModel"); QQmlListModel *listModel = qvariant_cast(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 o(modelComponent.create()); QQmlDelegateModel *delegateModel = qobject_cast(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 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>()); QVERIFY(qMetaTypeId>() > 0); std::forward_list 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()); } void tst_QQmlDelegateModel::doNotUnrefObjectUnderConstruction() { QQmlEngine engine; QQmlComponent component(&engine, testFileUrl("modifyObjectUnderConstruction.qml")); QVERIFY2(component.isReady(), qPrintable(component.errorString())); std::unique_ptr 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 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 object(component.create()); QVERIFY(object); QQuickItem *listview = object->findChild("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"