#include #include #include #include #include #include #include #include #include #include #include class TestMovableModel : 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(); } static constexpr QModelIndex InvalidIdx{}; private slots: void initializationTest() { QQmlEngine engine; ListModelWrapper sourceModel(engine, R"([ { "name": "A", "subname": "a1" }, { "name": "A", "subname": "a2" }, { "name": "B", "subname": "b1" }, { "name": "C", "subname": "c1" }, { "name": "C", "subname": "c2" }, { "name": "C", "subname": "c3" } ])"); MovableModel model; model.setSourceModel(sourceModel); QCOMPARE(model.synced(), true); QVERIFY(isSame(&model, sourceModel)); } void desyncOrderTest() { QQmlEngine engine; ListModelWrapper sourceModel(engine, R"([ { "name": "A", "subname": "a1" }, { "name": "A", "subname": "a2" }, { "name": "B", "subname": "b1" }, { "name": "C", "subname": "c1" }, { "name": "C", "subname": "c2" }, { "name": "C", "subname": "c3" } ])"); MovableModel model; model.setSourceModel(sourceModel); QSignalSpy syncedChangedSpy(&model, &MovableModel::syncedChanged); model.desyncOrder(); QCOMPARE(syncedChangedSpy.count(), 1); QCOMPARE(model.synced(), false); QVERIFY(isSame(&model, sourceModel)); model.setSourceModel(nullptr); QCOMPARE(syncedChangedSpy.count(), 2); QCOMPARE(model.synced(), true); QCOMPARE(model.rowCount(), 0); } void moveDownTest() { QQmlEngine engine; auto source = R"([ { "name": "A", "subname": "a1" }, { "name": "A", "subname": "a2" }, { "name": "B", "subname": "b1" }, { "name": "C", "subname": "c1" }, { "name": "C", "subname": "c2" }, { "name": "C", "subname": "c3" } ])"; ListModelWrapper sourceModel(engine, source); ListModelWrapper sourceModelCopy(engine, source); MovableModel model; model.setSourceModel(sourceModel); model.desyncOrder(); ModelSignalsSpy signalsSpy(&model); ModelSignalsSpy referenceSignalsSpy(sourceModelCopy); model.move(0, 2, 2); sourceModelCopy.move(0, 2, 2); QCOMPARE(signalsSpy.count(), 2); QCOMPARE(referenceSignalsSpy.count(), 2); QCOMPARE(signalsSpy.rowsAboutToBeMovedSpy.count(), 1); QCOMPARE(signalsSpy.rowsMovedSpy.count(), 1); QCOMPARE(referenceSignalsSpy.rowsAboutToBeMovedSpy.count(), 1); QCOMPARE(referenceSignalsSpy.rowsMovedSpy.count(), 1); QCOMPARE(signalsSpy.rowsAboutToBeMovedSpy.first(), referenceSignalsSpy.rowsAboutToBeMovedSpy.first()); QCOMPARE(signalsSpy.rowsMovedSpy.first(), referenceSignalsSpy.rowsMovedSpy.first()); ListModelWrapper expected(engine, R"([ { "name": "B", "subname": "b1" }, { "name": "C", "subname": "c1" }, { "name": "A", "subname": "a1" }, { "name": "A", "subname": "a2" }, { "name": "C", "subname": "c2" }, { "name": "C", "subname": "c3" } ])"); QVERIFY(isSame(sourceModelCopy, expected)); QVERIFY(isSame(&model, expected)); QVERIFY(isSame(&model, sourceModelCopy)); } void moveUpTest() { QQmlEngine engine; auto source = R"([ { "name": "A", "subname": "a1" }, { "name": "A", "subname": "a2" }, { "name": "B", "subname": "b1" }, { "name": "C", "subname": "c1" }, { "name": "C", "subname": "c2" }, { "name": "C", "subname": "c3" } ])"; ListModelWrapper sourceModel(engine, source); ListModelWrapper sourceModelCopy(engine, source); MovableModel model; model.setSourceModel(sourceModel); model.desyncOrder(); ModelSignalsSpy signalsSpy(&model); ModelSignalsSpy referenceSignalsSpy(sourceModelCopy); model.move(3, 1, 2); sourceModelCopy.move(3, 1, 2); QCOMPARE(signalsSpy.count(), 2); QCOMPARE(referenceSignalsSpy.count(), 2); QCOMPARE(signalsSpy.rowsAboutToBeMovedSpy.count(), 1); QCOMPARE(signalsSpy.rowsMovedSpy.count(), 1); QCOMPARE(referenceSignalsSpy.rowsAboutToBeMovedSpy.count(), 1); QCOMPARE(referenceSignalsSpy.rowsMovedSpy.count(), 1); QCOMPARE(signalsSpy.rowsAboutToBeMovedSpy.first(), referenceSignalsSpy.rowsAboutToBeMovedSpy.first()); QCOMPARE(signalsSpy.rowsMovedSpy.first(), referenceSignalsSpy.rowsMovedSpy.first()); ListModelWrapper expected(engine, R"([ { "name": "A", "subname": "a1" }, { "name": "C", "subname": "c1" }, { "name": "C", "subname": "c2" }, { "name": "A", "subname": "a2" }, { "name": "B", "subname": "b1" }, { "name": "C", "subname": "c3" } ])"); QVERIFY(isSame(sourceModelCopy, expected)); QVERIFY(isSame(&model, expected)); QVERIFY(isSame(&model, sourceModelCopy)); } void moveToEndTest() { QQmlEngine engine; auto source = R"([ { "name": "A", "subname": "a1" }, { "name": "A", "subname": "a2" }, { "name": "B", "subname": "b1" }, { "name": "C", "subname": "c1" }, { "name": "C", "subname": "c2" }, { "name": "C", "subname": "c3" } ])"; ListModelWrapper sourceModel(engine, source); ListModelWrapper sourceModelCopy(engine, source); MovableModel model; model.setSourceModel(sourceModel); model.desyncOrder(); ModelSignalsSpy signalsSpy(&model); ModelSignalsSpy referenceSignalsSpy(sourceModelCopy); model.move(1, 4, 2); sourceModelCopy.move(1, 4, 2); QCOMPARE(signalsSpy.count(), 2); QCOMPARE(referenceSignalsSpy.count(), 2); QCOMPARE(signalsSpy.rowsAboutToBeMovedSpy.count(), 1); QCOMPARE(signalsSpy.rowsMovedSpy.count(), 1); QCOMPARE(referenceSignalsSpy.rowsAboutToBeMovedSpy.count(), 1); QCOMPARE(referenceSignalsSpy.rowsMovedSpy.count(), 1); ListModelWrapper expected(engine, R"([ { "name": "A", "subname": "a1" }, { "name": "C", "subname": "c1" }, { "name": "C", "subname": "c2" }, { "name": "C", "subname": "c3" }, { "name": "A", "subname": "a2" }, { "name": "B", "subname": "b1" } ])"); QVERIFY(isSame(sourceModelCopy, expected)); QVERIFY(isSame(&model, expected)); QVERIFY(isSame(&model, sourceModelCopy)); } void moveToBeginningTest() { QQmlEngine engine; auto source = R"([ { "name": "A", "subname": "a1" }, { "name": "A", "subname": "a2" }, { "name": "B", "subname": "b1" }, { "name": "C", "subname": "c1" }, { "name": "C", "subname": "c2" }, { "name": "C", "subname": "c3" } ])"; ListModelWrapper sourceModel(engine, source); ListModelWrapper sourceModelCopy(engine, source); MovableModel model; model.setSourceModel(sourceModel); model.desyncOrder(); ModelSignalsSpy signalsSpy(&model); ModelSignalsSpy referenceSignalsSpy(sourceModelCopy); model.move(3, 0, 3); sourceModelCopy.move(3, 0, 3); QCOMPARE(signalsSpy.count(), 2); QCOMPARE(referenceSignalsSpy.count(), 2); QCOMPARE(signalsSpy.rowsAboutToBeMovedSpy.count(), 1); QCOMPARE(signalsSpy.rowsMovedSpy.count(), 1); QCOMPARE(referenceSignalsSpy.rowsAboutToBeMovedSpy.count(), 1); QCOMPARE(referenceSignalsSpy.rowsMovedSpy.count(), 1); ListModelWrapper expected(engine, R"([ { "name": "C", "subname": "c1" }, { "name": "C", "subname": "c2" }, { "name": "C", "subname": "c3" }, { "name": "A", "subname": "a1" }, { "name": "A", "subname": "a2" }, { "name": "B", "subname": "b1" } ])"); QVERIFY(isSame(sourceModelCopy, expected)); QVERIFY(isSame(&model, expected)); QVERIFY(isSame(&model, sourceModelCopy)); } void sortingTest() { QQmlEngine engine; auto source = R"([ { "name": "A", "subname": "a1" }, { "name": "A", "subname": "a2" }, { "name": "B", "subname": "b1" }, { "name": "C", "subname": "c1" }, { "name": "C", "subname": "c2" }, { "name": "C", "subname": "c3" } ])"; ListModelWrapper sourceModel(engine, source); ListModelWrapper sourceModelCopy(engine, source); QSortFilterProxyModel sfpm; sfpm.setSourceModel(sourceModel); MovableModel model; model.setSourceModel(&sfpm); model.desyncOrder(); model.move(2, 1); sourceModelCopy.move(2, 1); ModelSignalsSpy signalsSpy(&model); sfpm.setSortRole(1); sfpm.sort(0, Qt::DescendingOrder); ListModelWrapper expectedSorted(engine, R"([ { "name": "C", "subname": "c3" }, { "name": "C", "subname": "c2" }, { "name": "C", "subname": "c1" }, { "name": "B", "subname": "b1" }, { "name": "A", "subname": "a2" }, { "name": "A", "subname": "a1" } ])"); QCOMPARE(signalsSpy.count(), 0); QCOMPARE(sfpm.roleNames().value(1), "subname"); QVERIFY(isSame(&sfpm, expectedSorted)); QVERIFY(isSame(&model, sourceModelCopy)); } void insertionTest() { QQmlEngine engine; ListModelWrapper sourceModel(engine, R"([ { "name": "A", "subname": "a1" }, { "name": "A", "subname": "a2" }, { "name": "B", "subname": "b1" }, { "name": "C", "subname": "c1" }, { "name": "C", "subname": "c2" }, { "name": "C", "subname": "c3" } ])"); MovableModel model; model.setSourceModel(sourceModel); model.desyncOrder(); model.move(4, 1); SnapshotModel snapshot(model); QObject context; connect(&model, &QAbstractItemModel::rowsAboutToBeInserted, &context, [m = &model, s = &snapshot] { QVERIFY(isSame(m, s)); }); ModelSignalsSpy signalsSpy(&model); sourceModel.insert(3, QJsonArray { QJsonObject {{ "name", "D"}, { "subname", "d1" }}, QJsonObject {{ "name", "D"}, { "subname", "d2" }} }); ListModelWrapper expected(engine, R"([ { "name": "A", "subname": "a1" }, { "name": "C", "subname": "c2" }, { "name": "A", "subname": "a2" }, { "name": "D", "subname": "d1" }, { "name": "D", "subname": "d2" }, { "name": "B", "subname": "b1" }, { "name": "C", "subname": "c1" }, { "name": "C", "subname": "c3" } ])"); QCOMPARE(signalsSpy.count(), 2); QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.count(), 1); QCOMPARE(signalsSpy.rowsInsertedSpy.count(), 1); QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.first().at(0), QModelIndex{}); QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.first().at(1), 3); QCOMPARE(signalsSpy.rowsAboutToBeInsertedSpy.first().at(2), 4); QCOMPARE(signalsSpy.rowsInsertedSpy.first().at(0), QModelIndex{}); QCOMPARE(signalsSpy.rowsInsertedSpy.first().at(1), 3); QCOMPARE(signalsSpy.rowsInsertedSpy.first().at(2), 4); QVERIFY(isSame(&model, expected)); } void removalTest() { QQmlEngine engine; ListModelWrapper sourceModel(engine, R"([ { "name": "A", "subname": "a1" }, { "name": "A", "subname": "a2" }, { "name": "B", "subname": "b1" }, { "name": "C", "subname": "c1" }, { "name": "C", "subname": "c2" }, { "name": "C", "subname": "c3" } ])"); MovableModel model; model.setSourceModel(sourceModel); model.move(3, 0, 3); ListModelWrapper expectedIntermediate(engine, R"([ { "name": "C", "subname": "c1" }, { "name": "C", "subname": "c2" }, { "name": "C", "subname": "c3" }, { "name": "A", "subname": "a1" }, { "name": "A", "subname": "a2" }, { "name": "B", "subname": "b1" } ])"); QVERIFY(isSame(&model, expectedIntermediate)); ModelSignalsSpy signalsSpy(&model); sourceModel.remove(1, 4); ListModelWrapper expected(engine, R"([ { "name": "C", "subname": "c3" }, { "name": "A", "subname": "a1" } ])"); QVERIFY(isSame(&model, expected)); QCOMPARE(signalsSpy.count(), 4); QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.count(), 2); QCOMPARE(signalsSpy.rowsRemovedSpy.count(), 2); QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.at(0).at(1), 4); QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.at(0).at(2), 5); QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.at(1).at(0), QModelIndex{}); QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.at(1).at(1), 0); QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.at(1).at(2), 1); QCOMPARE(signalsSpy.rowsRemovedSpy.at(0).at(0), QModelIndex{}); QCOMPARE(signalsSpy.rowsRemovedSpy.at(0).at(1), 4); QCOMPARE(signalsSpy.rowsRemovedSpy.at(0).at(2), 5); QCOMPARE(signalsSpy.rowsRemovedSpy.at(1).at(0), QModelIndex{}); QCOMPARE(signalsSpy.rowsRemovedSpy.at(1).at(1), 0); QCOMPARE(signalsSpy.rowsRemovedSpy.at(1).at(2), 1); } void dataChangeTest() { QQmlEngine engine; ListModelWrapper sourceModel(engine, R"([ { "name": "A", "subname": "a1" }, { "name": "A", "subname": "a2" }, { "name": "B", "subname": "b1" }, { "name": "C", "subname": "c1" }, { "name": "C", "subname": "c2" }, { "name": "C", "subname": "c3" } ])"); MovableModel model; model.setSourceModel(sourceModel); model.desyncOrder(); ModelSignalsSpy signalsSpy(&model); sourceModel.setProperty(1, "subname", "a2_"); ListModelWrapper expected(engine, R"([ { "name": "A", "subname": "a1" }, { "name": "A", "subname": "a2_" }, { "name": "B", "subname": "b1" }, { "name": "C", "subname": "c1" }, { "name": "C", "subname": "c2" }, { "name": "C", "subname": "c3" } ])"); auto subnameRole = roleForName(model.roleNames(), "subname"); QCOMPARE(signalsSpy.count(), 1); QCOMPARE(signalsSpy.dataChangedSpy.count(), 1); QCOMPARE(signalsSpy.dataChangedSpy.first().at(0), model.index(0)); QCOMPARE(signalsSpy.dataChangedSpy.first().at(1), model.index(model.rowCount() - 1)); QCOMPARE(signalsSpy.dataChangedSpy.first().at(2).value>(), {subnameRole}); QVERIFY(isSame(&model, expected)); } void orderTest() { QQmlEngine engine; ListModelWrapper sourceModel(engine, R"([ { "name": "A", "subname": "a1" }, { "name": "A", "subname": "a2" }, { "name": "B", "subname": "b1" }, { "name": "C", "subname": "c1" }, { "name": "C", "subname": "c2" }, { "name": "C", "subname": "c3" } ])"); MovableModel model; model.setSourceModel(sourceModel); QVector expectedOrder = {0, 1, 2, 3, 4, 5}; QCOMPARE(model.order(), expectedOrder); sourceModel.move(0, 1); QCOMPARE(model.order(), expectedOrder); sourceModel.move(0, 1); // restore original source order model.move(0, 1); expectedOrder = {1, 0, 2, 3, 4, 5}; QCOMPARE(model.order(), expectedOrder); sourceModel.move(0, 1); expectedOrder = {0, 1, 2, 3, 4, 5}; QCOMPARE(model.order(), expectedOrder); sourceModel.move(0, 1); // restore original source order model.move(0, 1); expectedOrder = {0, 1, 2, 3, 4, 5}; QCOMPARE(model.order(), expectedOrder); model.move(1, 3, 2); expectedOrder = {0, 3, 4, 1, 2, 5}; QCOMPARE(model.order(), expectedOrder); sourceModel.remove(4); expectedOrder = {0, 3, 1, 2, 4}; QCOMPARE(model.order(), expectedOrder); } void invalidMoveTest() { QQmlEngine engine; ListModelWrapper sourceModel(engine, R"([ { "name": "A", "subname": "a1" }, { "name": "A", "subname": "a2" }, { "name": "B", "subname": "b1" }, { "name": "C", "subname": "c1" }, { "name": "C", "subname": "c2" }, { "name": "C", "subname": "c3" } ])"); MovableModel model; model.setSourceModel(sourceModel); { QTest::ignoreMessage(QtWarningMsg, "MovableModel: move: out of range"); model.move(2, -1); } { QTest::ignoreMessage(QtWarningMsg, "MovableModel: move: out of range"); model.move(-2, 1); } { QTest::ignoreMessage(QtWarningMsg, "MovableModel: move: out of range"); model.move(5, 0, 2); } { QTest::ignoreMessage(QtWarningMsg, "MovableModel: move: out of range"); model.move(0, 5, 2); } // QTest::failOnWarning(QRegularExpression(".?")); // Qt 6.3 sourceModel.move(0, 0, 2); } void resetSourceTest() { QQmlEngine engine; auto source1 = R"([ { "name": "A", "subname": "a1" }, { "name": "A", "subname": "a2" }, { "name": "B", "subname": "b1" }, { "name": "C", "subname": "c1" }, { "name": "C", "subname": "c2" }, { "name": "C", "subname": "c3" } ])"; auto source2 = R"([ { "name_": "A", "subname_": "a1" }, { "name_": "A", "subname_": "a2" } ])"; ListModelWrapper sourceModel1(engine, source1); ListModelWrapper sourceModel2(engine, source2); MovableModel model; model.setSourceModel(sourceModel1); model.desyncOrder(); QCOMPARE(model.synced(), false); ModelSignalsSpy signalsSpy(&model); QSignalSpy syncedChangedSpy(&model, &MovableModel::syncedChanged); model.setSourceModel(sourceModel2); QCOMPARE(signalsSpy.count(), 2); QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 1); QCOMPARE(signalsSpy.modelResetSpy.count(), 1); QCOMPARE(syncedChangedSpy.count(), 1); QCOMPARE(model.synced(), true); QCOMPARE(model.rowCount(), 2); QVERIFY(isSame(&model, sourceModel2)); } void syncOrderTest() { QQmlEngine engine; auto source = R"([ { "name": "A", "subname": "a1" }, { "name": "A", "subname": "a2" }, { "name": "B", "subname": "b1" }, { "name": "C", "subname": "c1" }, { "name": "C", "subname": "c2" }, { "name": "C", "subname": "c3" } ])"; ListModelWrapper sourceModel(engine, source); MovableModel model; { ModelSignalsSpy signalsSpy(&model); model.syncOrder(); QCOMPARE(signalsSpy.count(), 0); } model.setSourceModel(sourceModel); { ModelSignalsSpy signalsSpy(&model); model.syncOrder(); QCOMPARE(signalsSpy.count(), 0); } model.desyncOrder(); sourceModel.move(0, 2, 2); QVERIFY(!isSame(&model, sourceModel)); ModelSignalsSpy signalsSpy(&model); QSignalSpy syncedChangedSpy(&model, &MovableModel::syncedChanged); model.syncOrder(); QCOMPARE(signalsSpy.count(), 2); QCOMPARE(signalsSpy.layoutAboutToBeChangedSpy.count(), 1); QCOMPARE(signalsSpy.layoutChangedSpy.count(), 1); QCOMPARE(syncedChangedSpy.count(), 1); QCOMPARE(model.synced(), true); QVERIFY(isSame(&model, sourceModel)); } }; QTEST_MAIN(TestMovableModel) #include "tst_MovableModel.moc"