status-desktop/ui/StatusQ/tests/tst_MovableModel.cpp
Michał Cieślak e51667911d StatusQ: MovableModel proxy for setting custom order over source model
Proxy decorating source mode with additional method move(from, to, count)
similar to that available in ListModel. The custom order is stored within
a proxy, not altering the original model. May be useful whenever UI needs
to allow user to set custom order. Temporary state can be held in the
proxy, and send to the backend when changes are accepted.

Closes: #12686
2024-01-22 16:36:41 +01:00

578 lines
18 KiB
C++

#include <QSignalSpy>
#include <QTest>
#include <QJsonArray>
#include <QJsonObject>
#include <QQmlEngine>
#include <QSortFilterProxyModel>
#include <StatusQ/movablemodel.h>
#include <TestHelpers/listmodelwrapper.h>
#include <TestHelpers/modelsignalsspy.h>
#include <TestHelpers/modeltestutils.h>
#include <TestHelpers/snapshotmodel.h>
class TestMovableModel : public QObject
{
Q_OBJECT
int roleForName(const QHash<int, QByteArray>& 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.detached(), false);
QVERIFY(isSame(&model, sourceModel));
}
void detachTest()
{
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 detachChangedSpy(&model, &MovableModel::detachedChanged);
model.detach();
QCOMPARE(detachChangedSpy.count(), 1);
QCOMPARE(model.detached(), true);
QVERIFY(isSame(&model, sourceModel));
model.setSourceModel(nullptr);
QCOMPARE(detachChangedSpy.count(), 2);
QCOMPARE(model.detached(), false);
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.detach();
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.detach();
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.detach();
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.detach();
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.detach();
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.detach();
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.detach();
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<QVector<int>>(),
{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<int> 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);
}
};
QTEST_MAIN(TestMovableModel)
#include "tst_MovableModel.moc"