#include #include #include #include #include #include #include #include #include #include #include #include #include class TestSubmodelProxyModel: public QObject { Q_OBJECT int roleForName(const QHash& roles, const QByteArray& name) const { auto keys = roles.keys(name); if (keys.empty()) return -1; return keys.first(); } private slots: void basicTest() { QQmlEngine engine; QQmlComponent delegate(&engine); auto delegateData = R"( import QtQml 2.15 QtObject { property var count: submodel.count } )"; delegate.setData(delegateData, QUrl()); SubmodelProxyModel model; auto source = R"([ { balances: [ { balance: 4 } ], name: "name 1" }, { balances: [ { balance: 4 }, {balance: 43} ], name: "name 2" }, { balances: [], name: "name 3" } ])"; ListModelWrapper sourceModel(engine, source); QSignalSpy sourceModelChangedSpy( &model, &SubmodelProxyModel::sourceModelChanged); QSignalSpy delegateChangedSpy( &model, &SubmodelProxyModel::delegateModelChanged); QSignalSpy submodelRoleNameChangedSpy( &model, &SubmodelProxyModel::submodelRoleNameChanged); model.setSourceModel(sourceModel); model.setDelegateModel(&delegate); model.setSubmodelRoleName(QStringLiteral("balances")); QCOMPARE(sourceModelChangedSpy.count(), 1); QCOMPARE(delegateChangedSpy.count(), 1); QCOMPARE(submodelRoleNameChangedSpy.count(), 1); QCOMPARE(model.sourceModel(), sourceModel); QCOMPARE(model.delegateModel(), &delegate); QCOMPARE(model.submodelRoleName(), QStringLiteral("balances")); QCOMPARE(model.rowCount(), 3); QCOMPARE(model.data(model.index(0, 0), sourceModel.role("name")), "name 1"); QVERIFY(model.data(model.index(0, 0), sourceModel.role("balances")).isValid()); auto object = model.data(model.index(0, 0), sourceModel.role("balances")).value(); QVERIFY(object); auto context = QQmlEngine::contextForObject(object); QVERIFY(context->contextProperty("submodel").value() != nullptr); QCOMPARE(object->property("count"), 1); QCOMPARE(QQmlEngine::objectOwnership(object), QQmlEngine::CppOwnership); } void submodelTypeTest() { QQmlEngine engine; QQmlComponent delegate(&engine); auto delegateData = R"( import QtQml 2.15 QtObject { property var count: submodel.count } )"; delegate.setData(delegateData, QUrl()); SubmodelProxyModel model; auto source = R"([ { balances: [ { balance: 4 } ], name: "name 1" } ])"; ListModelWrapper sourceModel(engine, source); model.setSourceModel(sourceModel); model.setDelegateModel(&delegate); model.setSubmodelRoleName(QStringLiteral("balances")); QCOMPARE(model.rowCount(), 1); QVariant balances1 = model.data(model.index(0, 0), sourceModel.role("balances")); QVERIFY(balances1.isValid()); QVariant balances2 = model.data(model.index(0, 0), sourceModel.role("balances")); QVERIFY(balances2.isValid()); // SubmodelProxyModel may create proxy objects on demand, then first // call to data(...) returns freshly created object, the next calls // related to the same row should return cached object. It's important // to have QVariant type identical in both cases. E.g. returning raw // pointer in first call and pointer wrapped into QPointer in the next // one leads to problems in UI components in some scenarios even if // those QVariant types are automatically convertible. QCOMPARE(balances2.type(), balances1.type()); // Check if the same instance is returned. QCOMPARE(balances2.value(), balances1.value()); } void usingNonObjectSubmodelRoleTest() { QQmlEngine engine; QQmlComponent delegate(&engine); auto delegateData = R"( import QtQml 2.15 QtObject { property var count: submodel.count } )"; delegate.setData(delegateData, QUrl()); SubmodelProxyModel model; auto source = R"([ { balances: 1, name: "name 1" }, { balances: 2, name: "name 2" }, { balances: 3, name: "name 3" } ])"; ListModelWrapper sourceModel(engine, source); QTest::ignoreMessage(QtWarningMsg, "Submodel must be a QObject-based type!"); model.setSourceModel(sourceModel); model.setDelegateModel(&delegate); model.setSubmodelRoleName(QStringLiteral("balances")); QCOMPARE(model.rowCount(), 3); QVERIFY(model.data(model.index(0, 0), sourceModel.role("balances")).isValid()); } void deletingDelegateTest() { QQmlEngine engine; auto delegate = std::make_unique(&engine); delegate->setData(QByteArrayLiteral(R"( import QtQml 2.15 QtObject { property var sub: submodel } )"), QUrl()); SubmodelProxyModel model; ListModelWrapper sourceModel(engine, QJsonArray { QJsonObject {{ "balances", 11 }, { "name", "name 1" }}, QJsonObject {{ "balances", 12 }, { "name", "name 2" }}, QJsonObject {{ "balances", 123}, { "name", "name 3" }} }); model.setSourceModel(sourceModel); model.setDelegateModel(delegate.get()); model.setSubmodelRoleName(QStringLiteral("balances")); QSignalSpy delegateModelChangedSpy( &model, &SubmodelProxyModel::delegateModelChanged); QSignalSpy dataChangedSpy( &model, &SubmodelProxyModel::dataChanged); delegate.reset(); QCOMPARE(delegateModelChangedSpy.count(), 1); QCOMPARE(dataChangedSpy.count(), 1); QCOMPARE(model.rowCount(), 3); QCOMPARE(model.data(model.index(0, 0), sourceModel.role("balances")), 11); } void deletingSourceModelTest() { QQmlEngine engine; QQmlComponent delegate(&engine); delegate.setData(QByteArrayLiteral(R"( import QtQml 2.15 QtObject { property var sub: submodel } )"), QUrl()); SubmodelProxyModel model; auto sourceModel = std::make_unique(engine, QJsonArray { QJsonObject {{ "balances", 11 }, { "name", "name 1" }}, QJsonObject {{ "balances", 12 }, { "name", "name 2" }}, QJsonObject {{ "balances", 123}, { "name", "name 3" }} } ); model.setSourceModel(sourceModel->model()); model.setDelegateModel(&delegate); model.setSubmodelRoleName(QStringLiteral("balances")); sourceModel.reset(); QCOMPARE(model.rowCount(), 0); QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*")); QCOMPARE(model.data(model.index(0, 0), 0), {}); } void settingUndefinedSubmodelRoleNameTest() { QQmlEngine engine; auto delegate = std::make_unique(&engine); delegate->setData(QByteArrayLiteral(R"( import QtQml 2.15 QtObject { property var sub: submodel } )"), QUrl()); SubmodelProxyModel model; ListModelWrapper sourceModel(engine, QJsonArray { QJsonObject {{ "balances", 11 }, { "name", "name 1" }}, QJsonObject {{ "balances", 12 }, { "name", "name 2" }}, QJsonObject {{ "balances", 123}, { "name", "name 3" }} }); model.setSourceModel(sourceModel); model.setDelegateModel(delegate.get()); QTest::ignoreMessage(QtWarningMsg, "Submodel role not found!"); model.setSubmodelRoleName(QStringLiteral("undefined")); QCOMPARE(model.rowCount(), 3); } void addingNewRoleToTopLevelModelTest() { QQmlEngine engine; auto delegate = std::make_unique(&engine); delegate->setData(QByteArrayLiteral(R"( import QtQml.Models 2.15 ListModel { id: delegateRoot property var sub: submodel property int extraValue: submodel.rowCount() readonly property alias extraValueRole: delegateRoot.extraValue } )"), QUrl()); SubmodelProxyModel model; ListModelWrapper sourceModel(engine, R"([ { "balances": [], "name": "name 1" }, { "balances": [ { balance: 1 } ], "name": "name 2" }, { "balances": [], "name": "name 3" } ])"); model.setSourceModel(sourceModel); model.setDelegateModel(delegate.get()); model.setSubmodelRoleName(QStringLiteral("balances")); ListModelWrapper expected(engine, R"([ { "balances": [], "name": "name 1", "extraValue": 0 }, { "balances": [], "name": "name 2", "extraValue": 1 }, { "balances": [], "name": "name 3", "extraValue": 0 } ])"); QCOMPARE(model.rowCount(), 3); auto roles = model.roleNames(); QCOMPARE(roles.size(), 3); QVERIFY(isSame(&model, expected)); ModelSignalsSpy signalsSpy(&model); QVariant wrapperVariant = model.data(model.index(0, 0), roleForName(roles, "balances")); QObject* wrapper = wrapperVariant.value(); QVERIFY(wrapper != nullptr); wrapper->setProperty("extraValue", 42); ListModelWrapper expected2(engine, R"([ { "balances": [], "name": "name 1", "extraValue": 42 }, { "balances": [], "name": "name 2", "extraValue": 1 }, { "balances": [], "name": "name 3", "extraValue": 0 } ])"); // dataChanged signal emission is scheduled to event loop, not called // immediately QCOMPARE(signalsSpy.count(), 0); QVERIFY(QTest::qWaitFor([&signalsSpy]() { return signalsSpy.count() == 1; })); QCOMPARE(signalsSpy.count(), 1); QCOMPARE(signalsSpy.dataChangedSpy.count(), 1); QCOMPARE(signalsSpy.dataChangedSpy.at(0).at(0), model.index(0, 0)); QCOMPARE(signalsSpy.dataChangedSpy.at(0).at(1), model.index(model.rowCount() - 1, 0)); QVector expectedChangedRoles = { roleForName(roles, "extraValue") }; QCOMPARE(signalsSpy.dataChangedSpy.at(0).at(2).value>(), expectedChangedRoles); QVERIFY(isSame(&model, expected2)); } void additionalRoleDataChangedWhenEmptyTest() { QQmlEngine engine; auto delegate = std::make_unique(&engine); delegate->setData(QByteArrayLiteral(R"( import QtQml.Models 2.15 ListModel { property int extraValueRole: 0 } )"), QUrl()); ListModelWrapper sourceModel(engine, R"([ { "balances": [], "name": "name 1" } ])"); SubmodelProxyModel model; model.setSourceModel(sourceModel); model.setDelegateModel(delegate.get()); model.setSubmodelRoleName(QStringLiteral("balances")); QCOMPARE(model.rowCount(), 1); auto roles = model.roleNames(); QCOMPARE(roles.size(), 3); ModelSignalsSpy signalsSpy(&model); QVariant wrapperVariant = model.data(model.index(0, 0), roleForName(roles, "balances")); QObject* wrapper = wrapperVariant.value(); QVERIFY(wrapper != nullptr); // dataChanged signal emission is scheduled to event loop, not called // immediately. In the meantime the source may be cleared and then no // dataChanged event should be emited. wrapper->setProperty("extraValueRole", 42); sourceModel.remove(0); QCOMPARE(signalsSpy.count(), 2); QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.count(), 1); QCOMPARE(signalsSpy.rowsRemovedSpy.count(), 1); QTest::qWait(100); QCOMPARE(signalsSpy.count(), 2); } void multipleProxiesTest() { QSKIP("Not implemented yet. The goal is to make the proxy fully " "non-intrusive what will fix the isse pointed in this test."); QQmlEngine engine; auto delegate1 = std::make_unique(&engine); delegate1->setData(QByteArrayLiteral(R"( import QtQml.Models 2.15 ListModel { readonly property int myProp: 42 } )"), QUrl()); auto delegate2 = std::make_unique(&engine); delegate2->setData(QByteArrayLiteral(R"( import QtQml.Models 2.15 ListModel { readonly property int myProp: 11 } )"), QUrl()); ListModelWrapper sourceModel(engine, R"([ { "balances": [], "name": "name 1" }, { "balances": [], "name": "name 2" }, { "balances": [], "name": "name 3" } ])"); SubmodelProxyModel model1; model1.setSourceModel(sourceModel); model1.setDelegateModel(delegate1.get()); model1.setSubmodelRoleName(QStringLiteral("balances")); SubmodelProxyModel model2; model2.setSourceModel(sourceModel); model2.setDelegateModel(delegate2.get()); model2.setSubmodelRoleName(QStringLiteral("balances")); auto roles = model1.roleNames(); QCOMPARE(roles.size(), 2); QVariant wrapperVariant1 = model1.data(model1.index(0, 0), roleForName(roles, "balances")); QObject* wrapper1 = wrapperVariant1.value(); QCOMPARE(wrapper1->property("myProp"), 42); QVariant wrapperVariant2 = model2.data(model2.index(0, 0), roleForName(roles, "balances")); QObject* wrapper2 = wrapperVariant2.value(); QCOMPARE(wrapper2->property("myProp"), 11); } }; QTEST_MAIN(TestSubmodelProxyModel) #include "tst_SubmodelProxyModel.moc"