#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { // Workaround for https://bugreports.qt.io/browse/QTBUG-57971 (ListModel doesn't // emit modelReset when role names are initially set, therefore QIdentityProxyModel // doesn't update role names appropriately) class IdentityModel : public QIdentityProxyModel { public: QHash roleNames() const override { if (sourceModel()) return sourceModel()->roleNames(); return {}; } }; } // unnamed namespace class TestConcatModel: 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 initializationTest() { QQmlEngine engine; ConcatModel model; ListModelWrapper sourceModel1(engine, QJsonArray { QJsonObject {{ "key", 1}, { "balance", 11 }, { "name", "n1" }}, QJsonObject {{ "key", 2},{ "balance", 12 }, { "name", "n2" }}, QJsonObject {{ "key", 3},{ "balance", 13}, { "name", "n3" }}, }); ListModelWrapper sourceModel2(engine, QJsonArray {}); ListModelWrapper sourceModel3(engine, QJsonArray { QJsonObject {{ "balance", 14 }, { "name", "n4" }, { "color", "red"}}, QJsonObject {{ "balance", 15 }, { "name", "n5" }, { "color", "green"}}, QJsonObject {{ "balance", 16 }, { "name", "n6" }, { "color", "blue"}}, QJsonObject {{ "balance", 17 }, { "name", "n7" }, { "color", "pink"}}, }); ListModelWrapper sourceModel4(engine, QJsonArray {}); QQmlListProperty sources = model.sources(); SourceModel source1; source1.setModel(sourceModel1); SourceModel source2; source2.setModel(sourceModel2); SourceModel source3; source3.setModel(sourceModel3); SourceModel source4; source4.setModel(sourceModel4); sources.append(&sources, &source1); sources.append(&sources, &source2); sources.append(&sources, &source3); sources.append(&sources, &source4); QCOMPARE(model.rowCount(), 0); QCOMPARE(model.roleNames(), {}); QCOMPARE(model.index(0, 0).isValid(), false); model.componentComplete(); auto roles = model.roleNames(); QCOMPARE(model.rowCount(), 7); QCOMPARE(roles.size(), 5); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "key")), 1); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "key")), 2); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "key")), 3); QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "key")), {}); QCOMPARE(model.data(model.index(4, 0), roleForName(roles, "key")), {}); QCOMPARE(model.data(model.index(5, 0), roleForName(roles, "key")), {}); QCOMPARE(model.data(model.index(6, 0), roleForName(roles, "key")), {}); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "balance")), 11); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "balance")), 12); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "balance")), 13); QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "balance")), 14); QCOMPARE(model.data(model.index(4, 0), roleForName(roles, "balance")), 15); QCOMPARE(model.data(model.index(5, 0), roleForName(roles, "balance")), 16); QCOMPARE(model.data(model.index(6, 0), roleForName(roles, "balance")), 17); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "name")), "n1"); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "name")), "n2"); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "name")), "n3"); QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "name")), "n4"); QCOMPARE(model.data(model.index(4, 0), roleForName(roles, "name")), "n5"); QCOMPARE(model.data(model.index(5, 0), roleForName(roles, "name")), "n6"); QCOMPARE(model.data(model.index(6, 0), roleForName(roles, "name")), "n7"); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "color")), {}); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "color")), {}); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "color")), {}); QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "color")), "red"); QCOMPARE(model.data(model.index(4, 0), roleForName(roles, "color")), "green"); QCOMPARE(model.data(model.index(5, 0), roleForName(roles, "color")), "blue"); QCOMPARE(model.data(model.index(6, 0), roleForName(roles, "color")), "pink"); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "whichModel")), ""); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "whichModel")), ""); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "whichModel")), ""); QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "whichModel")), ""); QCOMPARE(model.data(model.index(4, 0), roleForName(roles, "whichModel")), ""); QCOMPARE(model.data(model.index(5, 0), roleForName(roles, "whichModel")), ""); QCOMPARE(model.data(model.index(6, 0), roleForName(roles, "whichModel")), ""); // out of bounds QCOMPARE(model.index(-1, 0).isValid(), false); QCOMPARE(model.index(7, 0).isValid(), false); auto roleKeys = roles.keys(); auto roleOutOfRange = *std::max_element(roleKeys.begin(), roleKeys.end()) + 1; QCOMPARE(model.data(model.index(0, 0), roleOutOfRange), {}); // getting source model and source model row QCOMPARE(model.sourceModel(0), sourceModel1); QCOMPARE(model.sourceModel(1), sourceModel1); QCOMPARE(model.sourceModel(2), sourceModel1); QCOMPARE(model.sourceModel(3), sourceModel3); QCOMPARE(model.sourceModel(4), sourceModel3); QCOMPARE(model.sourceModel(5), sourceModel3); QCOMPARE(model.sourceModel(6), sourceModel3); QCOMPARE(model.sourceModel(7), nullptr); QCOMPARE(model.sourceModel(-1), nullptr); QCOMPARE(model.sourceModelRow(0), 0); QCOMPARE(model.sourceModelRow(1), 1); QCOMPARE(model.sourceModelRow(2), 2); QCOMPARE(model.sourceModelRow(3), 0); QCOMPARE(model.sourceModelRow(4), 1); QCOMPARE(model.sourceModelRow(5), 2); QCOMPARE(model.sourceModelRow(6), 3); QCOMPARE(model.sourceModelRow(7), -1); QCOMPARE(model.sourceModelRow(-1), -1); // getting row by source model source model row QCOMPARE(model.fromSourceRow(nullptr, 0), -1); QCOMPARE(model.fromSourceRow(sourceModel1, 0), 0); QCOMPARE(model.fromSourceRow(sourceModel1, 1), 1); QCOMPARE(model.fromSourceRow(sourceModel1, 2), 2); QCOMPARE(model.fromSourceRow(sourceModel1, 3), -1); QCOMPARE(model.fromSourceRow(sourceModel1, -1), -1); QCOMPARE(model.fromSourceRow(sourceModel2, 0), -1); QCOMPARE(model.fromSourceRow(sourceModel3, 0), 3); QCOMPARE(model.fromSourceRow(sourceModel3, 1), 4); QCOMPARE(model.fromSourceRow(sourceModel3, 2), 5); QCOMPARE(model.fromSourceRow(sourceModel3, 3), 6); QCOMPARE(model.fromSourceRow(sourceModel3, 4), -1); } void dataChangeTest() { QQmlEngine engine; ConcatModel model; ListModelWrapper sourceModel1(engine, QJsonArray { QJsonObject {{ "key", 1}, { "balance", 11 }, { "name", "n1" }}, QJsonObject {{ "key", 2},{ "balance", 12 }, { "name", "n2" }}, QJsonObject {{ "key", 3},{ "balance", 13}, { "name", "n3" }}, }); ListModelWrapper sourceModel2(engine, QJsonArray {}); ListModelWrapper sourceModel3(engine, QJsonArray { QJsonObject {{ "balance", 14 }, { "name", "n4" }, { "color", "red"}}, QJsonObject {{ "balance", 15 }, { "name", "n5" }, { "color", "green"}}, QJsonObject {{ "balance", 16 }, { "name", "n6" }, { "color", "blue"}}, QJsonObject {{ "balance", 17 }, { "name", "n7" }, { "color", "pink"}}, }); ListModelWrapper sourceModel4(engine, QJsonArray {}); QQmlListProperty sources = model.sources(); SourceModel source1; source1.setModel(sourceModel1); SourceModel source2; source2.setModel(sourceModel2); SourceModel source3; source3.setModel(sourceModel3); SourceModel source4; source4.setModel(sourceModel4); sources.append(&sources, &source1); sources.append(&sources, &source2); sources.append(&sources, &source3); sources.append(&sources, &source4); QCOMPARE(model.rowCount(), 0); QCOMPARE(model.roleNames(), {}); QCOMPARE(model.index(0, 0).isValid(), false); model.componentComplete(); auto roles = model.roleNames(); // first non-empty source model modifications { QSignalSpy dataChangedSpy(&model, &ConcatModel::dataChanged); sourceModel1.setProperty(0, "key", 21); QCOMPARE(dataChangedSpy.count(), 1); QCOMPARE(dataChangedSpy.at(0).at(0), model.index(0, 0)); QCOMPARE(dataChangedSpy.at(0).at(1), model.index(0, 0)); QCOMPARE(dataChangedSpy.at(0).at(2).value>(), { roleForName(roles, "key") }); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "key")), 21); } { QSignalSpy dataChangedSpy(&model, &ConcatModel::dataChanged); sourceModel1.setProperty(1, "balance", 22); QCOMPARE(dataChangedSpy.count(), 1); QCOMPARE(dataChangedSpy.at(0).at(0), model.index(1, 0)); QCOMPARE(dataChangedSpy.at(0).at(1), model.index(1, 0)); QCOMPARE(dataChangedSpy.at(0).at(2).value>(), { roleForName(roles, "balance") }); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "balance")), 22); } { QSignalSpy dataChangedSpy(&model, &ConcatModel::dataChanged); sourceModel1.setProperty(2, "name", "n13"); QCOMPARE(dataChangedSpy.count(), 1); QCOMPARE(dataChangedSpy.at(0).at(0), model.index(2, 0)); QCOMPARE(dataChangedSpy.at(0).at(1), model.index(2, 0)); QCOMPARE(dataChangedSpy.at(0).at(2).value>(), { roleForName(roles, "name") }); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "name")), "n13"); } // second non-empty source model modifications { QSignalSpy dataChangedSpy(&model, &ConcatModel::dataChanged); sourceModel3.setProperty(0, "balance", 24); QCOMPARE(dataChangedSpy.count(), 1); QCOMPARE(dataChangedSpy.at(0).at(0), model.index(3, 0)); QCOMPARE(dataChangedSpy.at(0).at(1), model.index(3, 0)); QCOMPARE(dataChangedSpy.at(0).at(2).value>(), { roleForName(roles, "balance") }); QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "balance")), 24); } { QSignalSpy dataChangedSpy(&model, &ConcatModel::dataChanged); sourceModel3.setProperty(1, "name", "n25"); QCOMPARE(dataChangedSpy.count(), 1); QCOMPARE(dataChangedSpy.at(0).at(0), model.index(4, 0)); QCOMPARE(dataChangedSpy.at(0).at(1), model.index(4, 0)); QCOMPARE(dataChangedSpy.at(0).at(2).value>(), { roleForName(roles, "name") }); QCOMPARE(model.data(model.index(4, 0), roleForName(roles, "name")), "n25"); } { QSignalSpy dataChangedSpy(&model, &ConcatModel::dataChanged); sourceModel3.setProperty(2, "color", "orange"); QCOMPARE(dataChangedSpy.count(), 1); QCOMPARE(dataChangedSpy.at(0).at(0), model.index(5, 0)); QCOMPARE(dataChangedSpy.at(0).at(1), model.index(5, 0)); QCOMPARE(dataChangedSpy.at(0).at(2).value>(), { roleForName(roles, "color") }); QCOMPARE(model.data(model.index(5, 0), roleForName(roles, "color")), "orange"); } QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "key")), 21); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "key")), 2); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "key")), 3); QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "key")), {}); QCOMPARE(model.data(model.index(4, 0), roleForName(roles, "key")), {}); QCOMPARE(model.data(model.index(5, 0), roleForName(roles, "key")), {}); QCOMPARE(model.data(model.index(6, 0), roleForName(roles, "key")), {}); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "balance")), 11); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "balance")), 22); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "balance")), 13); QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "balance")), 24); QCOMPARE(model.data(model.index(4, 0), roleForName(roles, "balance")), 15); QCOMPARE(model.data(model.index(5, 0), roleForName(roles, "balance")), 16); QCOMPARE(model.data(model.index(6, 0), roleForName(roles, "balance")), 17); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "name")), "n1"); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "name")), "n2"); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "name")), "n13"); QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "name")), "n4"); QCOMPARE(model.data(model.index(4, 0), roleForName(roles, "name")), "n25"); QCOMPARE(model.data(model.index(5, 0), roleForName(roles, "name")), "n6"); QCOMPARE(model.data(model.index(6, 0), roleForName(roles, "name")), "n7"); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "color")), {}); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "color")), {}); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "color")), {}); QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "color")), "red"); QCOMPARE(model.data(model.index(4, 0), roleForName(roles, "color")), "green"); QCOMPARE(model.data(model.index(5, 0), roleForName(roles, "color")), "orange"); QCOMPARE(model.data(model.index(6, 0), roleForName(roles, "color")), "pink"); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "whichModel")), ""); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "whichModel")), ""); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "whichModel")), ""); QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "whichModel")), ""); QCOMPARE(model.data(model.index(4, 0), roleForName(roles, "whichModel")), ""); QCOMPARE(model.data(model.index(5, 0), roleForName(roles, "whichModel")), ""); QCOMPARE(model.data(model.index(6, 0), roleForName(roles, "whichModel")), ""); } void dataChangeOnNotTrackedRoleTest() { QQmlEngine engine; ConcatModel model; ListModelWrapper sourceModel1(engine, QJsonArray { QJsonObject {{ "key", 1}, { "balance", 11 }, { "name", "n1" }}, QJsonObject {{ "key", 2},{ "balance", 12 }, { "name", "n2" }}, QJsonObject {{ "key", 3},{ "balance", 13}, { "name", "n3" }}, }); ListModelWrapper sourceModel2(engine, QJsonArray {}); QQmlListProperty sources = model.sources(); SourceModel source1, source2; source1.setModel(sourceModel1); source2.setModel(sourceModel2); sources.append(&sources, &source1); sources.append(&sources, &source2); QCOMPARE(model.rowCount(), 0); QCOMPARE(model.roleNames(), {}); QCOMPARE(model.index(0, 0).isValid(), false); model.componentComplete(); QCOMPARE(model.rowCount(), 3); sourceModel2.append(QJsonArray { QJsonObject {{ "someRole", 1}}, QJsonObject {{ "someRole", 2}}, QJsonObject {{ "someRole", 3}}, QJsonObject {{ "someRole", 4}} }); QCOMPARE(model.rowCount(), 7); QSignalSpy dataChangedSpy(&model, &ConcatModel::dataChanged); sourceModel2.setProperty(0, "someRole", 42); QCOMPARE(dataChangedSpy.count(), 0); } void dataInsertionTest() { QQmlEngine engine; ConcatModel model; ListModelWrapper sourceModel1(engine, QJsonArray { QJsonObject {{ "key", 1}, { "balance", 11 }, { "name", "n1" }}, QJsonObject {{ "key", 2},{ "balance", 12 }, { "name", "n2" }}, QJsonObject {{ "key", 3},{ "balance", 13}, { "name", "n3" }}, }); ListModelWrapper sourceModel2(engine, QJsonArray {}); ListModelWrapper sourceModel3(engine, QJsonArray { QJsonObject {{ "balance", 14 }, { "name", "n4" }, { "color", "red"}}, QJsonObject {{ "balance", 15 }, { "name", "n5" }, { "color", "green"}}, QJsonObject {{ "balance", 16 }, { "name", "n6" }, { "color", "blue"}}, QJsonObject {{ "balance", 17 }, { "name", "n7" }, { "color", "pink"}}, }); ListModelWrapper sourceModel4(engine, QJsonArray {}); QQmlListProperty sources = model.sources(); SourceModel source1; source1.setModel(sourceModel1); SourceModel source2; source2.setModel(sourceModel2); SourceModel source3; source3.setModel(sourceModel3); SourceModel source4; source4.setModel(sourceModel4); sources.append(&sources, &source1); sources.append(&sources, &source2); sources.append(&sources, &source3); sources.append(&sources, &source4); QCOMPARE(model.rowCount(), 0); QCOMPARE(model.roleNames(), {}); QCOMPARE(model.index(0, 0).isValid(), false); model.componentComplete(); auto roles = model.roleNames(); // inserting into first model { QSignalSpy rowsInsertedSpy(&model, &ConcatModel::rowsInserted); sourceModel1.insert(0, QJsonObject { { "key", 200}, { "balance", 300 }, { "name", "n200" } }); QCOMPARE(rowsInsertedSpy.count(), 1); QCOMPARE(rowsInsertedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsInsertedSpy.at(0).at(1), 0); QCOMPARE(rowsInsertedSpy.at(0).at(2), 0); QCOMPARE(model.rowCount(), 8); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "key")), 200); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "balance")), 300); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "name")), "n200"); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "color")), {}); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "whichModel")), ""); } { QSignalSpy rowsInsertedSpy(&model, &ConcatModel::rowsInserted); sourceModel1.insert(2, QJsonObject {{ "key", 201}, { "balance", 301 }, { "name", "n201" }}); QCOMPARE(rowsInsertedSpy.count(), 1); QCOMPARE(rowsInsertedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsInsertedSpy.at(0).at(1), 2); QCOMPARE(rowsInsertedSpy.at(0).at(2), 2); QCOMPARE(model.rowCount(), 9); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "key")), 201); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "balance")), 301); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "name")), "n201"); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "color")), {}); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "whichModel")), ""); } // inserting into second, empty model { QSignalSpy rowsInsertedSpy(&model, &ConcatModel::rowsInserted); sourceModel2.insert(0, QJsonObject {{ "key", 202}, { "balance", 302 }, { "name", "n202" }}); QCOMPARE(rowsInsertedSpy.count(), 1); QCOMPARE(rowsInsertedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsInsertedSpy.at(0).at(1), 5); QCOMPARE(rowsInsertedSpy.at(0).at(2), 5); QCOMPARE(model.rowCount(), 10); QCOMPARE(model.data(model.index(5, 0), roleForName(roles, "key")), 202); QCOMPARE(model.data(model.index(5, 0), roleForName(roles, "balance")), 302); QCOMPARE(model.data(model.index(5, 0), roleForName(roles, "name")), "n202"); QCOMPARE(model.data(model.index(5, 0), roleForName(roles, "color")), {}); QCOMPARE(model.data(model.index(5, 0), roleForName(roles, "whichModel")), ""); } // inserting into third { QSignalSpy rowsInsertedSpy(&model, &ConcatModel::rowsInserted); sourceModel3.insert(2, QJsonObject { { "balance", 303 }, { "name", "n203" }, { "color", "brown" } }); QCOMPARE(rowsInsertedSpy.count(), 1); QCOMPARE(rowsInsertedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsInsertedSpy.at(0).at(1), 8); QCOMPARE(rowsInsertedSpy.at(0).at(2), 8); QCOMPARE(model.rowCount(), 11); QCOMPARE(model.data(model.index(8, 0), roleForName(roles, "key")), {}); QCOMPARE(model.data(model.index(8, 0), roleForName(roles, "balance")), 303); QCOMPARE(model.data(model.index(8, 0), roleForName(roles, "name")), "n203"); QCOMPARE(model.data(model.index(8, 0), roleForName(roles, "color")), "brown"); QCOMPARE(model.data(model.index(8, 0), roleForName(roles, "whichModel")), ""); } // inserting into forth, empty model { QSignalSpy rowsInsertedSpy(&model, &ConcatModel::rowsInserted); sourceModel4.insert(0, QJsonObject { { "key", 204 }, { "balance", 304 }, { "name", "n204" }, { "color", "black" } }); QCOMPARE(rowsInsertedSpy.count(), 1); QCOMPARE(rowsInsertedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsInsertedSpy.at(0).at(1), 11); QCOMPARE(rowsInsertedSpy.at(0).at(2), 11); QCOMPARE(model.rowCount(), 12); QCOMPARE(model.data(model.index(11, 0), roleForName(roles, "key")), 204); QCOMPARE(model.data(model.index(11, 0), roleForName(roles, "balance")), 304); QCOMPARE(model.data(model.index(11, 0), roleForName(roles, "name")), "n204"); QCOMPARE(model.data(model.index(11, 0), roleForName(roles, "color")), "black"); QCOMPARE(model.data(model.index(11, 0), roleForName(roles, "whichModel")), ""); } // inserting multiple items (first model) { QSignalSpy rowsInsertedSpy(&model, &ConcatModel::rowsInserted); sourceModel1.append(QJsonArray { QJsonObject {{ "key", 205}, { "balance", 305 }, { "name", "n205" }}, QJsonObject {{ "key", 206},{ "balance", 306 }, { "name", "n206" }} }); QCOMPARE(rowsInsertedSpy.count(), 1); QCOMPARE(rowsInsertedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsInsertedSpy.at(0).at(1), 5); QCOMPARE(rowsInsertedSpy.at(0).at(2), 6); QCOMPARE(model.rowCount(), 14); QCOMPARE(model.data(model.index(5, 0), roleForName(roles, "key")), 205); QCOMPARE(model.data(model.index(5, 0), roleForName(roles, "balance")), 305); QCOMPARE(model.data(model.index(5, 0), roleForName(roles, "name")), "n205"); QCOMPARE(model.data(model.index(5, 0), roleForName(roles, "color")), {}); QCOMPARE(model.data(model.index(5, 0), roleForName(roles, "whichModel")), ""); QCOMPARE(model.data(model.index(6, 0), roleForName(roles, "key")), 206); QCOMPARE(model.data(model.index(6, 0), roleForName(roles, "balance")), 306); QCOMPARE(model.data(model.index(6, 0), roleForName(roles, "name")), "n206"); QCOMPARE(model.data(model.index(6, 0), roleForName(roles, "color")), {}); QCOMPARE(model.data(model.index(6, 0), roleForName(roles, "whichModel")), ""); } } void dataInsertionToEmptyModelTest() { QQmlEngine engine; ConcatModel model; ListModelWrapper sourceModel1(engine); ListModelWrapper sourceModel2(engine); QQmlListProperty sources = model.sources(); SourceModel source1; source1.setModel(sourceModel1); SourceModel source2; source2.setModel(sourceModel2); sources.append(&sources, &source1); sources.append(&sources, &source2); QCOMPARE(model.rowCount(), 0); QCOMPARE(model.roleNames(), {}); QCOMPARE(model.index(0, 0).isValid(), false); model.componentComplete(); QCOMPARE(model.roleNames(), {}); { QSignalSpy rowsInsertedSpy(&model, &ConcatModel::rowsInserted); sourceModel1.append(QJsonArray { QJsonObject {{ "key", 1}, { "balance", 11 }, { "name", "n1" }}, QJsonObject {{ "key", 2},{ "balance", 12 }, { "name", "n2" }} }); QCOMPARE(rowsInsertedSpy.count(), 1); QCOMPARE(rowsInsertedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsInsertedSpy.at(0).at(1), 0); QCOMPARE(rowsInsertedSpy.at(0).at(2), 1); QCOMPARE(model.rowCount(), 2); auto roles = model.roleNames(); QCOMPARE(roles.size(), 4); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "key")), 1); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "balance")), 11); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "name")), "n1"); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "whichModel")), ""); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "key")), 2); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "balance")), 12); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "name")), "n2"); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "whichModel")), ""); } { QSignalSpy rowsInsertedSpy(&model, &ConcatModel::rowsInserted); sourceModel2.append(QJsonArray { QJsonObject {{ "key", 3}, { "color", "red" }}, QJsonObject {{ "key", 4}, { "color", "blue" }} }); QCOMPARE(rowsInsertedSpy.count(), 1); QCOMPARE(rowsInsertedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsInsertedSpy.at(0).at(1), 2); QCOMPARE(rowsInsertedSpy.at(0).at(2), 3); QCOMPARE(model.rowCount(), 4); auto roles = model.roleNames(); QCOMPARE(roles.size(), 4); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "key")), 1); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "balance")), 11); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "name")), "n1"); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "whichModel")), ""); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "key")), 2); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "balance")), 12); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "name")), "n2"); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "whichModel")), ""); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "key")), 3); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "balance")), {}); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "name")), {}); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "whichModel")), ""); QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "key")), 4); QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "balance")), {}); QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "name")), {}); QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "whichModel")), ""); } } void deferredNonEmptyModelSettingTest() { QQmlEngine engine; ConcatModel model; ListModelWrapper sourceModel1(engine, QJsonArray { QJsonObject {{ "key", 1}, { "color", "red" }}, QJsonObject {{ "key", 2}, { "color", "blue" }} }); ListModelWrapper sourceModel2(engine, QJsonArray { QJsonObject {{ "key", 3}, { "color", "orange" }}, QJsonObject {{ "key", 4}, { "color", "green" }} }); QQmlListProperty sources = model.sources(); SourceModel source1, source2; sources.append(&sources, &source1); sources.append(&sources, &source2); QCOMPARE(model.rowCount(), 0); QCOMPARE(model.roleNames(), {}); QCOMPARE(model.index(0, 0).isValid(), false); model.componentComplete(); QCOMPARE(model.rowCount(), 0); QCOMPARE(model.roleNames(), {}); { QSignalSpy rowsAboutToBeInsertedSpy( &model, &ConcatModel::rowsAboutToBeInserted); QSignalSpy rowsInsertedSpy(&model, &ConcatModel::rowsInserted); // checking validity inside rowsAboutToBeInserted signal { QObject context; connect(&model, &ConcatModel::rowsAboutToBeInserted, &context, [&model] { QCOMPARE(model.rowCount(), 0); }); source1.setModel(sourceModel1); } QCOMPARE(rowsAboutToBeInsertedSpy.count(), 1); QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(1), 0); QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(2), 1); QCOMPARE(rowsInsertedSpy.count(), 1); QCOMPARE(rowsInsertedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsInsertedSpy.at(0).at(1), 0); QCOMPARE(rowsInsertedSpy.at(0).at(2), 1); QCOMPARE(model.roleNames().size(), 3); QCOMPARE(model.rowCount(), 2); // check if connections are established correctly QSignalSpy dataChangedSpy(&model, &ConcatModel::dataChanged); sourceModel1.setProperty(1, "color", "white"); QCOMPARE(dataChangedSpy.count(), 1); QCOMPARE(dataChangedSpy.at(0).at(0), model.index(1, 0)); QCOMPARE(dataChangedSpy.at(0).at(1), model.index(1, 0)); } { QSignalSpy rowsInsertedSpy(&model, &ConcatModel::rowsInserted); { QObject context; connect(&model, &ConcatModel::rowsAboutToBeInserted, &context, [&model] { QCOMPARE(model.rowCount(), 2); }); source2.setModel(sourceModel2); } QCOMPARE(rowsInsertedSpy.count(), 1); QCOMPARE(rowsInsertedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsInsertedSpy.at(0).at(1), 2); QCOMPARE(rowsInsertedSpy.at(0).at(2), 3); QCOMPARE(model.roleNames().size(), 3); QCOMPARE(model.rowCount(), 4); // check if connections are established correctly QSignalSpy dataChangedSpy(&model, &ConcatModel::dataChanged); sourceModel2.setProperty(1, "color", "black"); QCOMPARE(dataChangedSpy.count(), 1); QCOMPARE(dataChangedSpy.at(0).at(0), model.index(3, 0)); QCOMPARE(dataChangedSpy.at(0).at(1), model.index(3, 0)); } } void deferredEmptyModelSettingTest() { QQmlEngine engine; ConcatModel model; ListModelWrapper sourceModel1(engine); ListModelWrapper sourceModel2(engine); QQmlListProperty sources = model.sources(); SourceModel source1, source2; sources.append(&sources, &source1); sources.append(&sources, &source2); QCOMPARE(model.rowCount(), 0); QCOMPARE(model.roleNames(), {}); QCOMPARE(model.index(0, 0).isValid(), false); model.componentComplete(); source1.setModel(sourceModel1); source2.setModel(sourceModel2); QCOMPARE(model.rowCount(), 0); QCOMPARE(model.roleNames(), {}); { QSignalSpy rowsAboutToBeInsertedSpy( &model, &ConcatModel::rowsAboutToBeInserted); QSignalSpy rowsInsertedSpy(&model, &ConcatModel::rowsInserted); // checking validity inside rowsAboutToBeInserted signal { QObject context; connect(&model, &ConcatModel::rowsAboutToBeInserted, &context, [&model] { QCOMPARE(model.rowCount(), 0); }); sourceModel2.append(QJsonArray { QJsonObject {{ "key", 1}, { "color", "red" }}, QJsonObject {{ "key", 2}, { "color", "blue" }} }); } QCOMPARE(rowsAboutToBeInsertedSpy.count(), 1); QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(1), 0); QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(2), 1); QCOMPARE(rowsInsertedSpy.count(), 1); QCOMPARE(rowsInsertedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsInsertedSpy.at(0).at(1), 0); QCOMPARE(rowsInsertedSpy.at(0).at(2), 1); QCOMPARE(model.roleNames().size(), 3); QCOMPARE(model.rowCount(), 2); // check if connections are established correctly QSignalSpy dataChangedSpy(&model, &ConcatModel::dataChanged); sourceModel2.setProperty(1, "color", "white"); QCOMPARE(dataChangedSpy.count(), 1); QCOMPARE(dataChangedSpy.at(0).at(0), model.index(1, 0)); QCOMPARE(dataChangedSpy.at(0).at(1), model.index(1, 0)); } { QSignalSpy rowsInsertedSpy(&model, &ConcatModel::rowsInserted); { QObject context; connect(&model, &ConcatModel::rowsAboutToBeInserted, &context, [&model] { QCOMPARE(model.rowCount(), 2); }); sourceModel1.append(QJsonArray { QJsonObject {{ "key", 3}, { "color", "green" }, { "value", 42}}, QJsonObject {{ "key", 4}, { "color", "white" }, { "value", 43}} }); } QCOMPARE(rowsInsertedSpy.count(), 1); QCOMPARE(rowsInsertedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsInsertedSpy.at(0).at(1), 0); QCOMPARE(rowsInsertedSpy.at(0).at(2), 1); QCOMPARE(model.roleNames().size(), 3); QCOMPARE(model.rowCount(), 4); // check if connections are established correctly QSignalSpy dataChangedSpy(&model, &ConcatModel::dataChanged); sourceModel2.setProperty(1, "color", "black"); QCOMPARE(dataChangedSpy.count(), 1); QCOMPARE(dataChangedSpy.at(0).at(0), model.index(3, 0)); QCOMPARE(dataChangedSpy.at(0).at(1), model.index(3, 0)); } } void settingModelsWithDifferentRolesTest() { QQmlEngine engine; ConcatModel model; ListModelWrapper sourceModel1(engine, QJsonArray { QJsonObject {{ "color", "red" }, { "key", 1}}, QJsonObject {{ "color", "blue" }, { "key", 2}} }); ListModelWrapper sourceModel2(engine, QJsonArray { QJsonObject {{ "key", 3}}, QJsonObject {{ "key", 4}} }); ListModelWrapper sourceModel3(engine, QJsonArray { QJsonObject {{ "key", 1}, { "color", "red" }, { "value", 42}}, QJsonObject {{ "key", 2}, { "color", "blue" }, { "value", 42}} }); QQmlListProperty sources = model.sources(); SourceModel source1, source2, source3; source1.setModel(sourceModel1); sources.append(&sources, &source1); sources.append(&sources, &source2); sources.append(&sources, &source3); QCOMPARE(model.rowCount(), 0); QCOMPARE(model.roleNames(), {}); QCOMPARE(model.index(0, 0).isValid(), false); model.componentComplete(); QCOMPARE(model.rowCount(), 2); QCOMPARE(model.roleNames().count(), 3); { QSignalSpy rowsAboutToBeInsertedSpy( &model, &ConcatModel::rowsAboutToBeInserted); QSignalSpy rowsInsertedSpy(&model, &ConcatModel::rowsInserted); // checking validity inside rowsAboutToBeInserted signal { QObject context; connect(&model, &ConcatModel::rowsAboutToBeInserted, &context, [&model] { QCOMPARE(model.rowCount(), 2); }); source2.setModel(sourceModel2); } QCOMPARE(rowsAboutToBeInsertedSpy.count(), 1); QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(1), 2); QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(2), 3); QCOMPARE(rowsInsertedSpy.count(), 1); QCOMPARE(rowsInsertedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsInsertedSpy.at(0).at(1), 2); QCOMPARE(rowsInsertedSpy.at(0).at(2), 3); QCOMPARE(model.roleNames().size(), 3); QCOMPARE(model.rowCount(), 4); // check if connections are established correctly QSignalSpy dataChangedSpy(&model, &ConcatModel::dataChanged); sourceModel1.setProperty(1, "color", "white"); QCOMPARE(dataChangedSpy.count(), 1); QCOMPARE(dataChangedSpy.at(0).at(0), model.index(1, 0)); QCOMPARE(dataChangedSpy.at(0).at(1), model.index(1, 0)); } { QSignalSpy rowsAboutToBeInsertedSpy( &model, &ConcatModel::rowsAboutToBeInserted); QSignalSpy rowsInsertedSpy(&model, &ConcatModel::rowsInserted); // checking validity inside rowsAboutToBeInserted signal { QObject context; connect(&model, &ConcatModel::rowsAboutToBeInserted, &context, [&model] { QCOMPARE(model.rowCount(), 4); }); source3.setModel(sourceModel3); } QCOMPARE(rowsAboutToBeInsertedSpy.count(), 1); QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(1), 4); QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(2), 5); QCOMPARE(rowsInsertedSpy.count(), 1); QCOMPARE(rowsInsertedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsInsertedSpy.at(0).at(1), 4); QCOMPARE(rowsInsertedSpy.at(0).at(2), 5); QCOMPARE(model.roleNames().size(), 3); QCOMPARE(model.rowCount(), 6); // check if connections are established correctly QSignalSpy dataChangedSpy(&model, &ConcatModel::dataChanged); sourceModel3.setProperty(0, "color", "white"); QCOMPARE(dataChangedSpy.count(), 1); QCOMPARE(dataChangedSpy.at(0).at(0), model.index(4, 0)); QCOMPARE(dataChangedSpy.at(0).at(1), model.index(4, 0)); } } void unsettingModelsTest() { QQmlEngine engine; ConcatModel model; ListModelWrapper sourceModel1(engine, QJsonArray { QJsonObject {{ "key", 1}, { "color", "red" }}, QJsonObject {{ "key", 2}, { "color", "blue" }} }); ListModelWrapper sourceModel2(engine, QJsonArray { QJsonObject {{ "key", 3}}, QJsonObject {{ "key", 4}} }); ListModelWrapper sourceModel3(engine, QJsonArray { QJsonObject {{ "key", 5}, { "color", "red" }, { "value", 42}}, QJsonObject {{ "key", 6}, { "color", "green" }, { "value", 43}} }); QQmlListProperty sources = model.sources(); SourceModel source1, source2, source3; source1.setModel(sourceModel1); source2.setModel(sourceModel2); source3.setModel(sourceModel3); sources.append(&sources, &source1); sources.append(&sources, &source2); sources.append(&sources, &source3); QCOMPARE(model.rowCount(), 0); QCOMPARE(model.roleNames(), {}); QCOMPARE(model.index(0, 0).isValid(), false); model.componentComplete(); QCOMPARE(model.rowCount(), 6); QCOMPARE(model.roleNames().count(), 4); { QSignalSpy rowsAboutToBeRemovedSpy(&model, &ConcatModel::rowsAboutToBeRemoved); QSignalSpy rowsRemovedSpy(&model, &ConcatModel::rowsRemoved); // checking validity inside rowsAboutToBeRemoved signal { QObject context; connect(&model, &ConcatModel::rowsAboutToBeRemoved, &context, [&model] { QCOMPARE(model.rowCount(), 6); }); source1.setModel(nullptr); } QCOMPARE(model.rowCount(), 4); QCOMPARE(rowsAboutToBeRemovedSpy.count(), 1); QCOMPARE(rowsRemovedSpy.count(), 1); QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(1), 0); QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(2), 1); QCOMPARE(rowsRemovedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsRemovedSpy.at(0).at(1), 0); QCOMPARE(rowsRemovedSpy.at(0).at(2), 1); } { QSignalSpy rowsAboutToBeRemovedSpy(&model, &ConcatModel::rowsAboutToBeRemoved); QSignalSpy rowsRemovedSpy(&model, &ConcatModel::rowsRemoved); // checking validity inside rowsAboutToBeRemoved signal { QObject context; connect(&model, &ConcatModel::rowsAboutToBeRemoved, &context, [&model] { QCOMPARE(model.rowCount(), 4); }); source3.setModel(nullptr); } QCOMPARE(rowsAboutToBeRemovedSpy.count(), 1); QCOMPARE(rowsRemovedSpy.count(), 1); QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(1), 2); QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(2), 3); QCOMPARE(rowsRemovedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsRemovedSpy.at(0).at(1), 2); QCOMPARE(rowsRemovedSpy.at(0).at(2), 3); } { QSignalSpy rowsAboutToBeRemovedSpy(&model, &ConcatModel::rowsAboutToBeRemoved); QSignalSpy rowsRemovedSpy(&model, &ConcatModel::rowsRemoved); // checking validity inside rowsAboutToBeRemoved signal { QObject context; connect(&model, &ConcatModel::rowsAboutToBeRemoved, &context, [&model] { QCOMPARE(model.rowCount(), 2); }); source2.setModel(nullptr); } QCOMPARE(rowsAboutToBeRemovedSpy.count(), 1); QCOMPARE(rowsRemovedSpy.count(), 1); QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(1), 0); QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(2), 1); QCOMPARE(rowsRemovedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsRemovedSpy.at(0).at(1), 0); QCOMPARE(rowsRemovedSpy.at(0).at(2), 1); } { QSignalSpy rowsAboutToBeRemovedSpy(&model, &ConcatModel::rowsAboutToBeRemoved); QSignalSpy rowsRemovedSpy(&model, &ConcatModel::rowsRemoved); source2.setModel(nullptr); QCOMPARE(rowsAboutToBeRemovedSpy.count(), 0); QCOMPARE(rowsRemovedSpy.count(), 0); } // check if signals are disconnected properly { QSignalSpy dataChangedSpy(&model, &ConcatModel::dataChanged); sourceModel1.setProperty(0, "key", 11); sourceModel2.setProperty(0, "key", 12); sourceModel3.setProperty(0, "key", 13); QCOMPARE(dataChangedSpy.count(), 0); } } void replacingModelsTest() { QQmlEngine engine; ConcatModel model; ListModelWrapper sourceModel1(engine, QJsonArray { QJsonObject {{ "key", 1}, { "color", "red" }}, QJsonObject {{ "key", 2}, { "color", "blue" }} }); ListModelWrapper sourceModel2(engine, QJsonArray { QJsonObject {{ "key", 3}}, QJsonObject {{ "key", 4}} }); ListModelWrapper sourceModel3(engine, QJsonArray { QJsonObject {{ "key", 5}, { "color", "red" }, { "value", 42}}, QJsonObject {{ "key", 6}, { "color", "green" }, { "value", 43}} }); ListModelWrapper sourceModel4(engine, QJsonArray { QJsonObject {{ "color", "orange" }, { "value", 44}, { "key", 7}}, QJsonObject {{ "color", "green" }, { "value", 45}, { "key", 8}}, QJsonObject {{ "color", "brown" }, { "value", 46}, { "key", 9}} }); QQmlListProperty sources = model.sources(); SourceModel source1, source2, source3; source1.setModel(sourceModel1); source2.setModel(sourceModel2); source3.setModel(sourceModel3); sources.append(&sources, &source1); sources.append(&sources, &source2); sources.append(&sources, &source3); QCOMPARE(model.rowCount(), 0); QCOMPARE(model.roleNames(), {}); QCOMPARE(model.index(0, 0).isValid(), false); model.componentComplete(); QCOMPARE(model.rowCount(), 6); QCOMPARE(model.roleNames().count(), 4); QSignalSpy rowsAboutToBeRemovedSpy(&model, &ConcatModel::rowsAboutToBeRemoved); QSignalSpy rowsRemovedSpy(&model, &ConcatModel::rowsRemoved); QSignalSpy rowsAboutToBeInsertedSpy( &model, &ConcatModel::rowsAboutToBeInserted); QSignalSpy rowsInsertedSpy(&model, &ConcatModel::rowsInserted); // checking validity inside rowsAboutToBeRemoved signal { QObject context; connect(&model, &ConcatModel::rowsAboutToBeRemoved, &context, [&model] { QCOMPARE(model.rowCount(), 6); }); connect(&model, &ConcatModel::rowsRemoved, &context, [&model] { QCOMPARE(model.rowCount(), 4); }); connect(&model, &ConcatModel::rowsAboutToBeInserted, &context, [&model] { QCOMPARE(model.rowCount(), 4); }); source2.setModel(sourceModel4); } QCOMPARE(model.rowCount(), 7); QCOMPARE(rowsAboutToBeRemovedSpy.count(), 1); QCOMPARE(rowsRemovedSpy.count(), 1); QCOMPARE(rowsAboutToBeInsertedSpy.count(), 1); QCOMPARE(rowsInsertedSpy.count(), 1); QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(1), 2); QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(2), 3); QCOMPARE(rowsRemovedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsRemovedSpy.at(0).at(1), 2); QCOMPARE(rowsRemovedSpy.at(0).at(2), 3); QCOMPARE(rowsAboutToBeInsertedSpy.count(), 1); QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(1), 2); QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(2), 4); QCOMPARE(rowsInsertedSpy.count(), 1); QCOMPARE(rowsInsertedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsInsertedSpy.at(0).at(1), 2); QCOMPARE(rowsInsertedSpy.at(0).at(2), 4); // check if previous model is disconnected QSignalSpy dataChangedSpy(&model, &ConcatModel::dataChanged); sourceModel2.setProperty(0, "key", 234); QCOMPARE(dataChangedSpy.count(), 0); // content validation auto roles = model.roleNames(); QCOMPARE(roles.count(), 4); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "key")), 1); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "key")), 2); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "key")), 7); QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "key")), 8); QCOMPARE(model.data(model.index(4, 0), roleForName(roles, "key")), 9); QCOMPARE(model.data(model.index(5, 0), roleForName(roles, "key")), 5); QCOMPARE(model.data(model.index(6, 0), roleForName(roles, "key")), 6); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "color")), "red"); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "color")), "blue"); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "color")), "orange"); QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "color")), "green"); QCOMPARE(model.data(model.index(4, 0), roleForName(roles, "color")), "brown"); QCOMPARE(model.data(model.index(5, 0), roleForName(roles, "color")), "red"); QCOMPARE(model.data(model.index(6, 0), roleForName(roles, "color")), "green"); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "value")), {}); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "value")), {}); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "value")), 44); QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "value")), 45); QCOMPARE(model.data(model.index(4, 0), roleForName(roles, "value")), 46); QCOMPARE(model.data(model.index(5, 0), roleForName(roles, "value")), 42); QCOMPARE(model.data(model.index(6, 0), roleForName(roles, "value")), 43); } void deletingModelsTest() { QQmlEngine engine; ConcatModel model; ListModelWrapper sourceModel1(engine, QJsonArray { QJsonObject {{ "key", 1}, { "color", "red" }}, QJsonObject {{ "key", 2}, { "color", "blue" }} }); auto sourceModel2 = std::make_unique(engine, QJsonArray { QJsonObject {{ "key", 3}}, QJsonObject {{ "key", 4}} }); ListModelWrapper sourceModel3(engine, QJsonArray { QJsonObject {{ "key", 5}, { "color", "red" }, { "value", 42}}, QJsonObject {{ "key", 6}, { "color", "green" }, { "value", 43}} }); QQmlListProperty sources = model.sources(); SourceModel source1, source2, source3; source1.setModel(sourceModel1); source2.setModel(*sourceModel2); source3.setModel(sourceModel3); sources.append(&sources, &source1); sources.append(&sources, &source2); sources.append(&sources, &source3); QCOMPARE(model.rowCount(), 0); QCOMPARE(model.roleNames(), {}); QCOMPARE(model.index(0, 0).isValid(), false); model.componentComplete(); auto roles = model.roleNames(); QCOMPARE(model.rowCount(), 6); QCOMPARE(roles.count(), 4); QSignalSpy rowsAboutToBeRemovedSpy(&model, &ConcatModel::rowsAboutToBeRemoved); QSignalSpy rowsRemovedSpy(&model, &ConcatModel::rowsRemoved); sourceModel2.reset(); QCOMPARE(model.rowCount(), 6); QCOMPARE(rowsAboutToBeRemovedSpy.count(), 0); QCOMPARE(rowsRemovedSpy.count(), 0); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "key")), 1); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "key")), 2); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "key")), {}); QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "key")), {}); QCOMPARE(model.data(model.index(4, 0), roleForName(roles, "key")), 5); QCOMPARE(model.data(model.index(5, 0), roleForName(roles, "key")), 6); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "whichModel")), ""); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "whichModel")), ""); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "whichModel")), ""); QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "whichModel")), ""); QCOMPARE(model.data(model.index(4, 0), roleForName(roles, "whichModel")), ""); QCOMPARE(model.data(model.index(5, 0), roleForName(roles, "whichModel")), ""); } void removalTest() { QQmlEngine engine; ConcatModel model; ListModelWrapper sourceModel1(engine, QJsonArray { QJsonObject {{ "key", 1}, { "color", "red" }}, QJsonObject {{ "key", 2}, { "color", "blue" }} }); ListModelWrapper sourceModel2(engine, QJsonArray { QJsonObject {{ "key", 3}}, QJsonObject {{ "key", 4}}, QJsonObject {{ "key", 5}} }); ListModelWrapper sourceModel3(engine, QJsonArray { QJsonObject {{ "key", 6}, { "color", "red" }, { "value", 42}}, QJsonObject {{ "key", 7}, { "color", "green" }, { "value", 43}} }); QQmlListProperty sources = model.sources(); SourceModel source1, source2, source3; source1.setModel(sourceModel1); source2.setModel(sourceModel2); source3.setModel(sourceModel3); sources.append(&sources, &source1); sources.append(&sources, &source2); sources.append(&sources, &source3); QCOMPARE(model.rowCount(), 0); QCOMPARE(model.roleNames(), {}); QCOMPARE(model.index(0, 0).isValid(), false); model.componentComplete(); auto roles = model.roleNames(); QCOMPARE(model.rowCount(), 7); QCOMPARE(roles.count(), 4); QSignalSpy rowsAboutToBeRemovedSpy(&model, &ConcatModel::rowsAboutToBeRemoved); QSignalSpy rowsRemovedSpy(&model, &ConcatModel::rowsRemoved); // checking validity inside rowsAboutToBeRemoved signal { QObject context; connect(&model, &ConcatModel::rowsAboutToBeRemoved, &context, [this, &model, &roles] { QCOMPARE(model.rowCount(), 7); QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "key")), 4); }); sourceModel2.remove(1, 2); } QCOMPARE(model.rowCount(), 5); QCOMPARE(rowsAboutToBeRemovedSpy.count(), 1); QCOMPARE(rowsRemovedSpy.count(), 1); QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(1), 3); QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(2), 4); QCOMPARE(rowsRemovedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsRemovedSpy.at(0).at(1), 3); QCOMPARE(rowsRemovedSpy.at(0).at(2), 4); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "key")), 1); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "key")), 2); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "key")), 3); QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "key")), 6); QCOMPARE(model.data(model.index(4, 0), roleForName(roles, "key")), 7); } void moveTest() { QQmlEngine engine; ConcatModel model; ListModelWrapper sourceModel1(engine, QJsonArray { QJsonObject {{ "key", 1}, { "color", "red" }}, QJsonObject {{ "key", 2}, { "color", "blue" }} }); ListModelWrapper sourceModel2(engine, QJsonArray { QJsonObject {{ "key", 3}}, QJsonObject {{ "key", 4}}, QJsonObject {{ "key", 5}} }); ListModelWrapper sourceModel3(engine, QJsonArray { QJsonObject {{ "key", 6}, { "color", "red" }, { "value", 42}}, QJsonObject {{ "key", 7}, { "color", "green" }, { "value", 43}} }); QQmlListProperty sources = model.sources(); SourceModel source1, source2, source3; source1.setModel(sourceModel1); source2.setModel(sourceModel2); source3.setModel(sourceModel3); sources.append(&sources, &source1); sources.append(&sources, &source2); sources.append(&sources, &source3); QCOMPARE(model.rowCount(), 0); QCOMPARE(model.roleNames(), {}); QCOMPARE(model.index(0, 0).isValid(), false); model.componentComplete(); auto roles = model.roleNames(); QCOMPARE(model.rowCount(), 7); QCOMPARE(roles.count(), 4); QSignalSpy rowsAboutToBeMovedSpy(&model, &ConcatModel::rowsAboutToBeMoved); QSignalSpy rowsMovedSpy(&model, &ConcatModel::rowsMoved); // checking validity inside rowsAboutToBeRemoved signal { QObject context; connect(&model, &ConcatModel::rowsAboutToBeMoved, &context, [this, &model, &roles] { QCOMPARE(model.rowCount(), 7); QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "key")), 4); }); sourceModel2.move(1, 0, 2); } QCOMPARE(model.rowCount(), 7); QCOMPARE(rowsAboutToBeMovedSpy.count(), 1); QCOMPARE(rowsMovedSpy.count(), 1); QCOMPARE(rowsAboutToBeMovedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsAboutToBeMovedSpy.at(0).at(1), 3); QCOMPARE(rowsAboutToBeMovedSpy.at(0).at(2), 4); QCOMPARE(rowsAboutToBeMovedSpy.at(0).at(3), QModelIndex{}); QCOMPARE(rowsAboutToBeMovedSpy.at(0).at(4), 2); QCOMPARE(rowsMovedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsMovedSpy.at(0).at(1), 3); QCOMPARE(rowsMovedSpy.at(0).at(2), 4); QCOMPARE(rowsMovedSpy.at(0).at(3), QModelIndex{}); QCOMPARE(rowsMovedSpy.at(0).at(4), 2); } void layoutChangedTest() { QQmlEngine engine; ConcatModel model; ListModelWrapper sourceModel1(engine, QJsonArray { QJsonObject {{ "key", 1}, { "color", "red" }}, QJsonObject {{ "key", 2}, { "color", "blue" }} }); ListModelWrapper sourceModel2(engine, QJsonArray { QJsonObject {{ "key", 3}}, QJsonObject {{ "key", 4}}, QJsonObject {{ "key", 5}} }); QQmlListProperty sources = model.sources(); SourceModel source1, source2, source3; source1.setModel(sourceModel1); source2.setModel(sourceModel2); sources.append(&sources, &source1); sources.append(&sources, &source2); QCOMPARE(model.rowCount(), 0); QCOMPARE(model.roleNames(), {}); QCOMPARE(model.index(0, 0).isValid(), false); model.componentComplete(); auto roles = model.roleNames(); QCOMPARE(model.rowCount(), 5); QCOMPARE(roles.count(), 3); // register types to avoid warnings regarding signal params qRegisterMetaType>(); qRegisterMetaType(); QSignalSpy layoutAboutToBeChangedSpy(&model, &ConcatModel::layoutAboutToBeChanged); QSignalSpy layoutChangedSpy(&model, &ConcatModel::layoutChanged); emit sourceModel1.model()->layoutAboutToBeChanged(); QCOMPARE(layoutAboutToBeChangedSpy.count(), 1); QCOMPARE(layoutChangedSpy.count(), 0); emit sourceModel1.model()->layoutChanged(); QCOMPARE(layoutAboutToBeChangedSpy.count(), 1); QCOMPARE(layoutChangedSpy.count(), 1); } void modelResetWhenEmptyTest() { QQmlEngine engine; ConcatModel model; ListModelWrapper sourceModel1(engine); ListModelWrapper sourceModel2(engine); ListModelWrapper sourceModel3(engine); ListModelWrapper sourceModel4(engine); ListModelWrapper sourceModel5(engine, QJsonArray { QJsonObject {{ "key", 1}, { "color", "red" }}, QJsonObject {{ "key", 2}, { "color", "blue" }} }); QQmlListProperty sources = model.sources(); SourceModel source1, source2, source3; IdentityModel proxy1, proxy2, proxy3; proxy1.setSourceModel(sourceModel1); proxy2.setSourceModel(sourceModel2); proxy3.setSourceModel(sourceModel3); source1.setModel(&proxy1); source2.setModel(&proxy2); source3.setModel(&proxy3); sources.append(&sources, &source1); sources.append(&sources, &source2); sources.append(&sources, &source3); QCOMPARE(model.rowCount(), 0); QCOMPARE(model.roleNames(), {}); QCOMPARE(model.index(0, 0).isValid(), false); model.componentComplete(); QCOMPARE(model.rowCount(), 0); QCOMPARE(model.roleNames(), {}); { QSignalSpy modelAboutToBeResetSpy(&model, &ConcatModel::modelAboutToBeReset); QSignalSpy modelResetSpy(&model, &ConcatModel::modelReset); QSignalSpy rowsAboutToBeInsertedSpy(&model, &ConcatModel::rowsAboutToBeInserted); QSignalSpy rowsInsertedSpy(&model, &ConcatModel::rowsInserted); proxy2.setSourceModel(sourceModel4); QCOMPARE(modelAboutToBeResetSpy.count(), 0); QCOMPARE(modelResetSpy.count(), 0); QCOMPARE(rowsAboutToBeInsertedSpy.count(), 0); QCOMPARE(rowsInsertedSpy.count(), 0); QCOMPARE(model.rowCount(), 0); QCOMPARE(model.roleNames(), {}); } { QSignalSpy modelAboutToBeResetSpy(&model, &ConcatModel::modelAboutToBeReset); QSignalSpy modelResetSpy(&model, &ConcatModel::modelReset); QSignalSpy rowsAboutToBeInsertedSpy(&model, &ConcatModel::rowsAboutToBeInserted); QSignalSpy rowsInsertedSpy(&model, &ConcatModel::rowsInserted); // checking validity inside rowsAboutToBeInserted signal { QObject context; connect(&model, &ConcatModel::rowsAboutToBeInserted, &context, [&model] { QCOMPARE(model.rowCount(), 0); }); proxy2.setSourceModel(sourceModel5); } QCOMPARE(modelAboutToBeResetSpy.count(), 0); QCOMPARE(modelResetSpy.count(), 0); QCOMPARE(rowsAboutToBeInsertedSpy.count(), 1); QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(1), 0); QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(2), 1); QCOMPARE(rowsInsertedSpy.count(), 1); QCOMPARE(rowsInsertedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsInsertedSpy.at(0).at(1), 0); QCOMPARE(rowsInsertedSpy.at(0).at(2), 1); auto roles = model.roleNames(); QCOMPARE(model.rowCount(), 2); QCOMPARE(roles.count(), 3); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "key")), 1); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "key")), 2); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "color")), "red"); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "color")), "blue"); } } void modelResetWhenNotEmptyTest() { QQmlEngine engine; ConcatModel model; ListModelWrapper sourceModel1(engine, QJsonArray { QJsonObject {{ "key", 1}, { "color", "red" }}, QJsonObject {{ "key", 2}, { "color", "blue" }} }); ListModelWrapper sourceModel2(engine, QJsonArray { QJsonObject {{ "key", 3}}, QJsonObject {{ "key", 4}}, QJsonObject {{ "key", 5}} }); ListModelWrapper sourceModel3(engine); ListModelWrapper sourceModel4(engine); ListModelWrapper sourceModel5(engine, QJsonArray { QJsonObject {{ "color", "red" }, { "name", "a" }, { "key", 11}}, QJsonObject {{ "color", "blue" }, { "name", "b" }, { "key", 12}}, QJsonObject {{ "color", "pink" }, { "name", "c" }, { "key", 13}} }); QQmlListProperty sources = model.sources(); SourceModel source1, source2, source3; IdentityModel proxy1, proxy2, proxy3; proxy1.setSourceModel(sourceModel1); proxy2.setSourceModel(sourceModel2); proxy3.setSourceModel(sourceModel3); source1.setModel(&proxy1); source2.setModel(&proxy2); source3.setModel(&proxy3); sources.append(&sources, &source1); sources.append(&sources, &source2); sources.append(&sources, &source3); QCOMPARE(model.rowCount(), 0); QCOMPARE(model.roleNames(), {}); QCOMPARE(model.index(0, 0).isValid(), false); model.componentComplete(); auto roles = model.roleNames(); QCOMPARE(model.rowCount(), 5); QCOMPARE(roles.count(), 3); // reset to empty model { QSignalSpy modelAboutToBeResetSpy(&model, &ConcatModel::modelAboutToBeReset); QSignalSpy modelResetSpy(&model, &ConcatModel::modelReset); QSignalSpy rowsAboutToBeInsertedSpy(&model, &ConcatModel::rowsAboutToBeInserted); QSignalSpy rowsInsertedSpy(&model, &ConcatModel::rowsInserted); QSignalSpy rowsAboutToBeRemovedSpy(&model, &ConcatModel::rowsAboutToBeRemoved); QSignalSpy rowsRemovedSpy(&model, &ConcatModel::rowsRemoved); // checking validity inside rowsAboutToBeRemoved signal { QObject context; connect(&model, &ConcatModel::rowsAboutToBeRemoved, &context, [this, &model, &roles] { QCOMPARE(model.rowCount(), 5); QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "key")), 4); QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "color")), {}); }); proxy2.setSourceModel(sourceModel4); } QCOMPARE(modelAboutToBeResetSpy.count(), 0); QCOMPARE(modelResetSpy.count(), 0); QCOMPARE(rowsAboutToBeInsertedSpy.count(), 0); QCOMPARE(rowsInsertedSpy.count(), 0); QCOMPARE(rowsAboutToBeRemovedSpy.count(), 1); QCOMPARE(rowsRemovedSpy.count(), 1); QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(1), 2); QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(2), 4); QCOMPARE(model.rowCount(), 2); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "key")), 1); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "key")), 2); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "color")), "red"); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "color")), "blue"); // insert some data to check if roles are re-initialized properly sourceModel4.append(QJsonArray { QJsonObject {{ "color", "purple"}, { "key", 3} }, QJsonObject {{ "color", "green" }, { "key", 4}} }); QCOMPARE(model.rowCount(), 4); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "key")), 1); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "key")), 2); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "key")), 3); QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "key")), 4); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "color")), "red"); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "color")), "blue"); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "color")), "purple"); QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "color")), "green"); sourceModel4.clear(); } // reset to not empty model { QSignalSpy modelAboutToBeResetSpy(&model, &ConcatModel::modelAboutToBeReset); QSignalSpy modelResetSpy(&model, &ConcatModel::modelReset); QSignalSpy rowsAboutToBeRemovedSpy(&model, &ConcatModel::rowsAboutToBeRemoved); QSignalSpy rowsRemovedSpy(&model, &ConcatModel::rowsRemoved); QSignalSpy rowsAboutToBeInsertedSpy(&model, &ConcatModel::rowsAboutToBeInserted); QSignalSpy rowsInsertedSpy(&model, &ConcatModel::rowsInserted); // checking validity inside rowsAboutToBeRemoved, rowsRemoved and // rowsAboutToBeInserted signals { QObject context; connect(&model, &ConcatModel::rowsAboutToBeRemoved, &context, [this, &model, &roles] { QCOMPARE(model.rowCount(), 2); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "key")), 1); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "key")), 2); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "color")), "red"); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "color")), "blue"); }); connect(&model, &ConcatModel::rowsRemoved, &context, [this, &model, &roles] { QCOMPARE(model.rowCount(), 0); }); connect(&model, &ConcatModel::rowsAboutToBeInserted, &context, [this, &model, &roles] { QCOMPARE(model.rowCount(), 0); }); proxy1.setSourceModel(sourceModel5); } QCOMPARE(modelAboutToBeResetSpy.count(), 0); QCOMPARE(modelResetSpy.count(), 0); QCOMPARE(rowsAboutToBeRemovedSpy.count(), 1); QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(1), 0); QCOMPARE(rowsAboutToBeRemovedSpy.at(0).at(2), 1); QCOMPARE(rowsRemovedSpy.count(), 1); QCOMPARE(rowsAboutToBeInsertedSpy.count(), 1); QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(1), 0); QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(2), 2); QCOMPARE(rowsInsertedSpy.count(), 1); QCOMPARE(model.rowCount(), 3); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "key")), 11); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "key")), 12); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "key")), 13); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "color")), "red"); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "color")), "blue"); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "color")), "pink"); } } void sameModelUsedMultipleTimesTest() { QQmlEngine engine; ConcatModel model; ListModelWrapper sourceModel(engine, QJsonArray { QJsonObject {{ "key", 1}, { "color", "red" }}, QJsonObject {{ "key", 2}, { "color", "blue" }} }); QQmlListProperty sources = model.sources(); SourceModel source1, source2, source3; source1.setModel(sourceModel); source2.setModel(sourceModel); source3.setModel(sourceModel); sources.append(&sources, &source1); sources.append(&sources, &source2); sources.append(&sources, &source3); QCOMPARE(model.rowCount(), 0); QCOMPARE(model.roleNames(), {}); QCOMPARE(model.index(0, 0).isValid(), false); model.componentComplete(); auto roles = model.roleNames(); QCOMPARE(model.rowCount(), 6); QCOMPARE(roles.count(), 3); QSignalSpy rowsAboutToBeInsertedSpy( &model, &ConcatModel::rowsAboutToBeInserted); QSignalSpy rowsInsertedSpy(&model, &ConcatModel::rowsInserted); sourceModel.insert(1, QJsonObject {{ "key", 15 }, { "color", "black" }}); QCOMPARE(model.rowCount(), 9); QCOMPARE(rowsAboutToBeInsertedSpy.count(), 3); QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(1), 1); QCOMPARE(rowsAboutToBeInsertedSpy.at(0).at(2), 1); QCOMPARE(rowsAboutToBeInsertedSpy.at(1).at(0), QModelIndex{}); QCOMPARE(rowsAboutToBeInsertedSpy.at(1).at(1), 3); QCOMPARE(rowsAboutToBeInsertedSpy.at(1).at(2), 3); QCOMPARE(rowsAboutToBeInsertedSpy.at(2).at(0), QModelIndex{}); QCOMPARE(rowsAboutToBeInsertedSpy.at(2).at(1), 5); QCOMPARE(rowsAboutToBeInsertedSpy.at(2).at(2), 5); QCOMPARE(rowsInsertedSpy.count(), 3); QCOMPARE(rowsInsertedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(rowsInsertedSpy.at(0).at(1), 5); QCOMPARE(rowsInsertedSpy.at(0).at(2), 5); QCOMPARE(rowsInsertedSpy.at(1).at(0), QModelIndex{}); QCOMPARE(rowsInsertedSpy.at(1).at(1), 3); QCOMPARE(rowsInsertedSpy.at(1).at(2), 3); QCOMPARE(rowsInsertedSpy.at(2).at(0), QModelIndex{}); QCOMPARE(rowsInsertedSpy.at(2).at(1), 1); QCOMPARE(rowsInsertedSpy.at(2).at(2), 1); QSignalSpy dataChangedSpy(&model, &ConcatModel::dataChanged); sourceModel.setProperty(0, "key", 0); QCOMPARE(model.rowCount(), 9); QCOMPARE(dataChangedSpy.count(), 3); QCOMPARE(dataChangedSpy.at(0).at(0), model.index(0, 0)); QCOMPARE(dataChangedSpy.at(0).at(1), model.index(0, 0)); QCOMPARE(dataChangedSpy.at(0).at(2).value>(), { roleForName(roles, "key") }); QCOMPARE(dataChangedSpy.at(1).at(0), model.index(3, 0)); QCOMPARE(dataChangedSpy.at(1).at(1), model.index(3, 0)); QCOMPARE(dataChangedSpy.at(1).at(2).value>(), { roleForName(roles, "key") }); QCOMPARE(dataChangedSpy.at(2).at(0), model.index(6, 0)); QCOMPARE(dataChangedSpy.at(2).at(1), model.index(6, 0)); QCOMPARE(dataChangedSpy.at(2).at(2).value>(), { roleForName(roles, "key") }); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "key")), 0); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "key")), 15); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "key")), 2); QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "key")), 0); QCOMPARE(model.data(model.index(4, 0), roleForName(roles, "key")), 15); QCOMPARE(model.data(model.index(5, 0), roleForName(roles, "key")), 2); QCOMPARE(model.data(model.index(6, 0), roleForName(roles, "key")), 0); QCOMPARE(model.data(model.index(7, 0), roleForName(roles, "key")), 15); QCOMPARE(model.data(model.index(8, 0), roleForName(roles, "key")), 2); } void expectedRolesTest() { QQmlEngine engine; ConcatModel model; ListModelWrapper sourceModel1(engine, QJsonArray { QJsonObject {{ "key", 1}}, QJsonObject {{ "key", 2}}, QJsonObject {{ "key", 3}} }); ListModelWrapper sourceModel2(engine, QJsonArray { QJsonObject {{ "key", 4}, { "color", "red" }, { "name", "name 1"}}, QJsonObject {{ "key", 5}, { "color", "blue" }, { "name", "name 2"}} }); ListModelWrapper sourceModel3(engine, QJsonArray { QJsonObject {{ "key", 6}, { "color", "red" }, { "value", 42}}, QJsonObject {{ "key", 7}, { "color", "green" }, { "value", 43}} }); QQmlListProperty sources = model.sources(); SourceModel source1, source2, source3; source1.setModel(sourceModel1); sources.append(&sources, &source1); sources.append(&sources, &source2); sources.append(&sources, &source3); model.setExpectedRoles({"key", "color", "value"}); QCOMPARE(model.rowCount(), 0); QCOMPARE(model.roleNames(), {}); QCOMPARE(model.index(0, 0).isValid(), false); model.componentComplete(); auto roles = model.roleNames(); QCOMPARE(model.rowCount(), 3); QCOMPARE(roles.size(), 4); std::set roleNamesSet(roles.cbegin(), roles.cend()); std::set expectedRoleNamesSet({"key", "color", "value", "whichModel"}); QCOMPARE(roleNamesSet, expectedRoleNamesSet); source2.setModel(sourceModel2); source3.setModel(sourceModel3); QCOMPARE(model.rowCount(), 7); } void markerRoleTest() { QQmlEngine engine; ConcatModel model; ListModelWrapper sourceModel1(engine, QJsonArray { QJsonObject {{ "key", 1}, { "balance", 11 }, { "name", "n1" }}, QJsonObject {{ "key", 2},{ "balance", 12 }, { "name", "n2" }}, QJsonObject {{ "key", 3},{ "balance", 13}, { "name", "n3" }}, }); ListModelWrapper sourceModel2(engine, QJsonArray {}); ListModelWrapper sourceModel3(engine, QJsonArray { QJsonObject {{ "balance", 14 }, { "name", "n4" }, { "color", "red"}}, QJsonObject {{ "balance", 15 }, { "name", "n5" }, { "color", "green"}}, QJsonObject {{ "balance", 16 }, { "name", "n6" }, { "color", "blue"}}, QJsonObject {{ "balance", 17 }, { "name", "n7" }, { "color", "pink"}}, }); ListModelWrapper sourceModel4(engine, QJsonArray {}); QQmlListProperty sources = model.sources(); SourceModel source1, source2, source3, source4; source1.setModel(sourceModel1); source2.setModel(sourceModel2); source3.setModel(sourceModel3); source4.setModel(sourceModel4); source1.setMarkerRoleValue("model 1"); source2.setMarkerRoleValue("model 2"); source3.setMarkerRoleValue("model 3"); source4.setMarkerRoleValue("model 4"); sources.append(&sources, &source1); sources.append(&sources, &source2); sources.append(&sources, &source3); sources.append(&sources, &source4); QCOMPARE(model.rowCount(), 0); QCOMPARE(model.roleNames(), {}); QCOMPARE(model.index(0, 0).isValid(), false); model.setMarkerRoleName("marker"); model.componentComplete(); QCOMPARE(model.markerRoleName(), "marker"); QCOMPARE(source1.markerRoleValue(), "model 1"); QCOMPARE(source2.markerRoleValue(), "model 2"); QCOMPARE(source3.markerRoleValue(), "model 3"); QCOMPARE(source4.markerRoleValue(), "model 4"); QTest::ignoreMessage(QtWarningMsg, "Property \"markerRoleName\" is intended to be " "initialized once before roles initialization " "and not modified later."); model.setMarkerRoleName("marker2"); QCOMPARE(model.markerRoleName(), "marker"); auto roles = model.roleNames(); QCOMPARE(model.rowCount(), 7); QCOMPARE(roles.size(), 5); QVERIFY(roleForName(roles, "marker") != -1); QCOMPARE(model.data(model.index(0, 0), roleForName(roles, "marker")), "model 1"); QCOMPARE(model.data(model.index(1, 0), roleForName(roles, "marker")), "model 1"); QCOMPARE(model.data(model.index(2, 0), roleForName(roles, "marker")), "model 1"); QCOMPARE(model.data(model.index(3, 0), roleForName(roles, "marker")), "model 3"); QCOMPARE(model.data(model.index(4, 0), roleForName(roles, "marker")), "model 3"); QCOMPARE(model.data(model.index(5, 0), roleForName(roles, "marker")), "model 3"); QCOMPARE(model.data(model.index(6, 0), roleForName(roles, "marker")), "model 3"); { QSignalSpy dataChangedSpy(&model, &ConcatModel::dataChanged); source1.setMarkerRoleValue("model 1_"); QCOMPARE(dataChangedSpy.count(), 1); QCOMPARE(dataChangedSpy.at(0).at(0), model.index(0, 0)); QCOMPARE(dataChangedSpy.at(0).at(1), model.index(2, 0)); QCOMPARE(dataChangedSpy.at(0).at(2).value>(), { roleForName(roles, "marker") }); } { QSignalSpy dataChangedSpy(&model, &ConcatModel::dataChanged); source2.setMarkerRoleValue("model 2_"); QCOMPARE(dataChangedSpy.count(), 0); } { QSignalSpy dataChangedSpy(&model, &ConcatModel::dataChanged); source3.setMarkerRoleValue("model 3_"); QCOMPARE(dataChangedSpy.count(), 1); QCOMPARE(dataChangedSpy.at(0).at(0), model.index(3, 0)); QCOMPARE(dataChangedSpy.at(0).at(1), model.index(6, 0)); QCOMPARE(dataChangedSpy.at(0).at(2).value>(), { roleForName(roles, "marker") }); } { QSignalSpy dataChangedSpy(&model, &ConcatModel::dataChanged); source4.setMarkerRoleValue("model 4_"); QCOMPARE(dataChangedSpy.count(), 0); } } void sortingTest() { QQmlEngine engine; auto source1 = R"([ { "name": "D", "subname": "d1" }, { "name": "A", "subname": "a1" }, { "name": "B", "subname": "b1" }, { "name": "C", "subname": "c1" }, { "name": "B", "subname": "b2" }, { "name": "C", "subname": "c2" } ])"; auto source2 = R"([ { "name": "A", "subname": "a1" }, { "name": "G", "subname": "g1" }, { "name": "F", "subname": "f1" }, { "name": "C", "subname": "c1" } ])"; ListModelWrapper sourceModel1(engine, source1); ListModelWrapper sourceModel2(engine, source2); QSortFilterProxyModel sfpm1; sfpm1.setSourceModel(sourceModel1); QSortFilterProxyModel sfpm2; sfpm2.setSourceModel(sourceModel2); ConcatModel model; QQmlListProperty sources = model.sources(); SourceModel sm1, sm2; sm1.setModel(&sfpm1); sm2.setModel(&sfpm2); sources.append(&sources, &sm1); sources.append(&sources, &sm2); model.componentComplete(); ModelSignalsSpy signalsSpy(&model); PersistentIndexesTester indexesTester(&model); sfpm1.setSortRole(1); sfpm1.sort(0, Qt::AscendingOrder); sfpm2.setSortRole(1); sfpm2.sort(0, Qt::DescendingOrder); auto roles = model.roleNames(); QCOMPARE(model.data(model.index(0), roleForName(roles, "subname")), "a1"); QCOMPARE(model.data(model.index(1), roleForName(roles, "subname")), "b1"); QCOMPARE(model.data(model.index(2), roleForName(roles, "subname")), "b2"); QCOMPARE(model.data(model.index(3), roleForName(roles, "subname")), "c1"); QCOMPARE(model.data(model.index(4), roleForName(roles, "subname")), "c2"); QCOMPARE(model.data(model.index(5), roleForName(roles, "subname")), "d1"); QCOMPARE(model.data(model.index(6), roleForName(roles, "subname")), "g1"); QCOMPARE(model.data(model.index(7), roleForName(roles, "subname")), "f1"); QCOMPARE(model.data(model.index(8), roleForName(roles, "subname")), "c1"); QCOMPARE(model.data(model.index(9), roleForName(roles, "subname")), "a1"); QCOMPARE(sfpm1.roleNames().value(1), "subname"); QVERIFY(indexesTester.compare()); } void layoutChangeWithRemovalTest() { ConcatModel model; TestModel sourceModel1({ { "name", { "name 1_1", "name 1_2", "name 1_3", "name 1_4" }} }); TestModel sourceModel2({ { "name", { "name 2_1", "name 2_2", "name 2_3" }} }); QQmlListProperty sources = model.sources(); SourceModel source1, source2; source1.setModel(&sourceModel1); source2.setModel(&sourceModel2); sources.append(&sources, &source1); sources.append(&sources, &source2); model.componentComplete(); QCOMPARE(model.rowCount(), 7); QPersistentModelIndex pmi1 = model.index(0); QPersistentModelIndex pmi2 = model.index(1); QPersistentModelIndex pmi3 = model.index(2); QPersistentModelIndex pmi4 = model.index(3); QPersistentModelIndex pmi5 = model.index(4); QPersistentModelIndex pmi6 = model.index(5); QPersistentModelIndex pmi7 = model.index(6); sourceModel1.removeEverySecond(); QCOMPARE(model.rowCount(), 5); QCOMPARE(pmi1.isValid(), false); QCOMPARE(pmi2.isValid(), true); QCOMPARE(pmi3.isValid(), false); QCOMPARE(pmi4.isValid(), true); QCOMPARE(pmi5.isValid(), true); QCOMPARE(pmi6.isValid(), true); QCOMPARE(pmi7.isValid(), true); QCOMPARE(pmi2.row(), 0); QCOMPARE(pmi4.row(), 1); QCOMPARE(pmi5.row(), 2); QCOMPARE(pmi6.row(), 3); QCOMPARE(pmi7.row(), 4); sourceModel2.removeEverySecond(); QCOMPARE(model.rowCount(), 3); auto roles = model.roleNames(); QCOMPARE(model.data(model.index(0), roleForName(roles, "name")), "name 1_2"); QCOMPARE(model.data(model.index(1), roleForName(roles, "name")), "name 1_4"); QCOMPARE(model.data(model.index(2), roleForName(roles, "name")), "name 2_2"); QCOMPARE(pmi1.isValid(), false); QCOMPARE(pmi2.isValid(), true); QCOMPARE(pmi3.isValid(), false); QCOMPARE(pmi4.isValid(), true); QCOMPARE(pmi5.isValid(), false); QCOMPARE(pmi6.isValid(), true); QCOMPARE(pmi7.isValid(), false); QCOMPARE(pmi2.row(), 0); QCOMPARE(pmi4.row(), 1); QCOMPARE(pmi6.row(), 2); } void sfpmTest() { // This test reproduces in C++ situation that happens in the following // QML code. The initial filtering is notified from a SFPM as // a layoutAboutToBeChanged/layoutChanged signals with row count change. // Concat model must manage properly row count update and persistent // indexes. // // import QtQuick 2.15 // import QtQuick.Controls 2.15 // import StatusQ 0.1 // import SortFilterProxyModel 0.2 // Item { // ListModel { // id: src // ListElement { name: "A" } // ListElement { name: "D" } // ListElement { name: "A" } // ListElement { name: "Z" } // ListElement { name: "A" } // } // SortFilterProxyModel { // id: sfpm // sourceModel: src // filters: ValueFilter { // roleName: "name" // value: "A" // } // } // ConcatModel { // id: concat // sources: SourceModel { model: sfpm } // } // Flow { // Repeater { // model: concat // Button { text: model.name } // } // } // } QQmlEngine engine; TestModel sourceModel({ { "name", { "A", "B", "A" }}, { "subname", { "a1", "b1", "a2" }} }); qqsfpm::QQmlSortFilterProxyModel sfpm; ConcatModel model; ModelSignalsSpy signalsSpy(&sfpm); sfpm.classBegin(); model.classBegin(); qqsfpm::ValueFilter filter; filter.setRoleName("name"); filter.setValue("A"); sfpm.appendFilter(&filter); QList layoutAboutToBeChangedSizes; QList layoutChangedSizes; connect(&sfpm, &QAbstractItemModel::layoutAboutToBeChanged, this, [&sfpm, &layoutAboutToBeChangedSizes]() { layoutAboutToBeChangedSizes << sfpm.rowCount(); }); connect(&sfpm, &QAbstractItemModel::layoutChanged, this, [&sfpm, &layoutChangedSizes]() { layoutChangedSizes << sfpm.rowCount(); }); QQmlListProperty sources = model.sources(); SourceModel sm; sm.setModel(&sfpm); sources.append(&sources, &sm); sfpm.setSourceModel(&sourceModel); // The crucial part is the fact that concat model is completed before // SFPM. It triggers filtering reported via // layoutAboutToBeChanged/layoutChanged. The same behavior can be observed // also for two SFPMs, it's not specific to ConcatModel. model.componentComplete(); sfpm.componentComplete(); QVERIFY(layoutAboutToBeChangedSizes.size() > 0); QVERIFY(layoutAboutToBeChangedSizes.size() == layoutChangedSizes.size()); // Checking whether the assumed circumstances have occurred. There should // be at least one pair of layoutAboutToBeChanged/layoutChanged where // on layoutAboutToBeChanged the count is smaller than in the following // layoutChanged. QList diffs; for (auto i = 0; i < layoutAboutToBeChangedSizes.size(); i++) diffs << std::abs(layoutAboutToBeChangedSizes[i] - layoutChangedSizes[i]); QVERIFY(std::accumulate(diffs.cbegin(), diffs.cend(), 0) != 0); QCOMPARE(model.rowCount(), 2); auto roles = model.roleNames(); QCOMPARE(model.data(model.index(0), roleForName(roles, "subname")), "a1"); QCOMPARE(model.data(model.index(1), roleForName(roles, "subname")), "a2"); } }; QTEST_MAIN(TestConcatModel) #include "tst_ConcatModel.moc"